structuraphp / structura
Architectural testing for PHP
Requires
- php: ^8.2|^8.3|^8.4
- nikic/php-parser: ^5.4
- symfony/console: ^7.2
- symfony/filesystem: ^7.2
- symfony/finder: ^7.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.69
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12.0
- rector/rector: ^2.0
- shipmonk/composer-dependency-analyser: ^1.8
This package is not auto-updated.
Last update: 2025-06-03 20:07:47 UTC
README
About
Structura is an architectural testing tool for PHP, designed to help developers maintain a clean and consistent code structure.
Requirements
PHP version
Version PHP | Structura 0.x |
---|---|
<= 8.1 | â Unsupported |
8.2 / 8.3 / 8.4 | â Supported |
Installation
Using Composer
composer required --dev structuraphp/structura
Usage
Configuration
Create the configuration file required for running the tests:
php structura init
At the root of your project, add the namespace and directory for your architecture tests:
return static function (StructuraConfig $config): void { $config->archiRootNamespace( '<MY_NAMESPACE>\Tests\Architecture', // namespace 'tests/Architecture', // test directory ); };
Make test
After creating and completing your configuration file, you can use the command to create architecture tests:
php bin/structura make
Here's a simple example of architecture testing for your DTOs:
use StructuraPhp\Structura\Asserts\ToExtendNothing; use StructuraPhp\Structura\Attributes\TestDox; use StructuraPhp\Structura\Expr; use StructuraPhp\Structura\Testing\TestBuilder; use Symfony\Component\Finder\Finder; final class TestDto extends TestBuilder { #[TestDox('Asserts rules Dto architecture')] public function testAssertArchitectureRules(): void { $this ->allClasses() ->fromDir( 'app/Dto', fn(Finder $finder) => $finder->depth('== 0') ) ->that($this->conditionThat(...)) ->except($this->exception(...)) ->should($this->should(...)); } private function that(Expr $expr): void { // The rules will only apply to classes (ignore traits, enums, interfaces, etc.) $expr->toBeClasses(); } private function exception(Except $except): void { // These classes will be ignored in the tests $except ->byClassname( className: [ FooDto::class, BarDto::class, ], expression: ToExtendNothing::class ); } private function should(Expr $expr): void { $expr ->toBeFinal() ->toBeReadonly() ->toHaveSuffix('Dto') ->toExtendsNothing() ->toHaveMethod('fromArray') ->toImplement(\JsonSerializable::class); } }
fromDir() and fromRaw()
Start with the fromDir()
method, which takes the path of the files to be analysed.
It can take a second closure parameter
to customise the finder:
->fromDir( 'src/Dto', static fn(Finder $finder): Finder => $finder->depth('== 0') )
fromRaw()
method can be used to test PHP code in the form of a string:
->fromRaw('<?php use ArrayAccess; use Depend\Bap; use Depend\Bar; class Foo implements \Stringable { public function __construct(ArrayAccess $arrayAccess) {} public function __toString(): string { return $this->arrayAccess['foo'] ?? throw new \Exception(); } }')
that()
Specifies rules for targeting class analysis, optional functionality:
->that(static fn(Expr $expr): Expr => $expr->toBeClasses())
except()
Ignores class rules, can be used as a baseline, optional functionality:
->except(static fn(Except $except): Except => $except ->byClassname( className: [ FooDto::class, BarDto::class, ], expression: ToExtendNothing::class ) )
should()
List of architecture rules, required functionality:
->should(static fn(Expr $expr): Expr => $expr ->toBeFinal() ->toBeReadonly() ->toHaveSuffix('Dto') ->toExtendsNothing() ->toHaveMethod('fromArray') ->toImplement(\JsonSerializable::class) )
First run
To run the architecture tests, execute the following command:
php bin/structura analyze
Assertions
- đ§Ŧ Types
- đ Dependencies
- 𧲠Relation
- đ Method
- đļī¸ Naming
- đšī¸ Other
- đī¸ Operators
toBeAbstract()
$this ->allClasses() ->fromRaw('<?php abstract class Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeAbstract(), );
toBeAnonymousClasses()
$this ->allClasses() ->fromRaw('<?php new class {};') ->should( static fn (Expr $assert): Expr => $assert->toBeAnonymousClasses(), );
toBeClasses()
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeClasses(), );
toBeEnums()
$this ->allClasses() ->fromRaw('<?php enum Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeEnums(), );
toBeFinal()
$this ->allClasses() ->fromRaw('<?php final class Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeFinal(), );
toBeInterfaces()
$this ->allClasses() ->fromRaw('<?php interface Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeInterfaces(), );
toBeInvokable()
$this ->allClasses() ->fromRaw('<?php class Foo { public function __invoke() {} }') ->should( static fn (Expr $assert): Expr => $assert->toBeInvokable(), );
toBeReadonly()
$this ->allClasses() ->fromRaw('<?php readonly class Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeReadonly(), );
toBeTraits()
$this ->allClasses() ->fromRaw('<?php trait Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeTraits(), );
dependsOnlyOn()
You can use regexes to select dependencies.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOn( names: [ArrayAccess::class, /* ... */], patterns: ['App\Dto.+', /* ... */], ) );
dependsOnlyOnAttribut()
If you use the rule classes toHaveAttribute(), they are included by default in the permitted dependencies.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnAttribut( names: [\Attribute::class, /* ... */], patterns: ['Attributes\Custom.+', /* ... */], ) );
dependsOnlyOnImplementation()
If you use the rule classes toImplement() and toOnlyImplement() they are included by default in the permitted dependencies.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnImplementation( names: [\ArrayAccess::class, /* ... */], patterns: ['Contracts\Dto.+', /* ... */], ) );
dependsOnlyOnInheritance()
If you use the rule classes toExtend() they are included by default in the permitted dependencies.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnInheritance( names: [Controller::class, /* ... */], patterns: ['Controllers\Admin.+', /* ... */], ) );
dependsOnlyOnUseTrait()
If you use the rule classes toUseTrait() and toOnlyUseTrait() they are included by default in the permitted dependencies.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnUseTrait( names: [\HasFactor::class, /* ... */], patterns: ['Concerns\Models.+', /* ... */], ) );
toNotDependsOn()
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->toNotDependsOn( names: [ArrayAccess::class, /* ... */], patterns: ['App\Dto.+', /* ... */], ) );
You can use regexes to select dependencies
toExtend()
$this ->allClasses() ->fromRaw('<?php class Foo extends \Exception {}') ->should(fn(Expr $expr) => $expr->toExtend(Exception::class));
toExtendsNothing()
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should(fn(Expr $expr) => $expr->toExtendsNothing());
toImplement()
$this ->allClasses() ->fromRaw('<?php class Foo implements \ArrayAccess, \JsonSerializable {}') ->should(fn(Expr $expr) => $expr->toImplement(ArrayAccess::class));
toImplementNothing()
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should(fn(Expr $expr) => $expr->toImplementNothing());
toOnlyImplement()
$this ->allClasses() ->fromRaw('<?php class Foo implements \ArrayAccess {}') ->should(fn(Expr $expr) => $expr->toOnlyImplement(ArrayAccess::class));
toUseTrait()
$this ->allClasses() ->fromRaw('<?php class Foo { use Bar, Baz; }') ->should(fn(Expr $expr) => $expr->toUseTrait(Bar::class));
toNotUseTrait()
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should(fn(Expr $expr) => $expr->toNotUseTrait());
toOnlyUseTrait()
$this ->allClasses() ->fromRaw('<?php class Foo { use Bar; }') ->should(fn(Expr $expr) => $expr->toOnlyUseTrait(Bar::class));
toHaveMethod()
$this ->allClasses() ->fromRaw('<?php class Foo { public function bar() {} }') ->should(fn(Expr $expr) => $expr->toHaveMethod('bar'));
toHaveConstructor()
$this ->allClasses() ->should(fn(Expr $expr) => $expr->toHaveConstructor());
toHaveDestructor()
$this ->allClasses() ->should(fn(Expr $expr) => $expr->toHaveDestructor());
toHavePrefix()
$this ->allClasses() ->fromRaw('<?php class ExempleFoo {}') ->should(fn(Expr $expr) => $expr->toHavePrefix('Exemple'));
toHaveSuffix()
$this ->allClasses() ->fromRaw('<?php class FooExemple {}') ->should(fn(Expr $expr) => $expr->toHaveSuffix('Exemple'));
toUseStrictTypes()
$this ->allClasses() ->fromRaw('<?php declare(strict_types=1); class Foo {}') ->should(fn(Expr $expr) => $expr->toUseStrictTypes());
toUseDeclare()
$this ->allClasses() ->fromRaw('<?php declare(encoding='ISO-8859-1'); class Foo {}') ->should(fn(Expr $expr) => $expr->toUseDeclare('encoding', 'ISO-8859-1'));
toHaveAttribute()
$this ->allClasses() ->fromRaw('<?php #[\Deprecated] class Foo {}') ->should(fn(Expr $expr) => $expr->toHaveAttribute(Deprecated::class));
and()
To be valid, all the rules contained in the and()
method must meet the requirements.
// if Foo interface extends ArrayAccess and (JsonSerializable and Countable) $this ->allClasses() ->fromRaw('<?php interface Foo extends ArrayAccess, JsonSerializable, Countable {}') ->should(fn(Expr $expr) => $expr ->toBeInterfaces() ->toExtend(ArrayAccess::class) ->and(fn(Expr $expr) => $expr ->toExtend(JsonSerializable::class) ->toExtend(Countable::class) ) );
or()
To be valid at least one of the rules contained in the or()
method must meet the requirements.
// if Foo class implements ArrayAccess and (JsonSerializable or Countable) $this ->allClasses() ->fromRaw('<?php class Foo implements ArrayAccess, JsonSerializable {}') ->should(fn(Expr $expr) => $expr ->toBeClasses() ->toImplement(ArrayAccess::class) ->or(fn(Expr $expr) => $expr ->toImplement(JsonSerializable::class) ->toImplement(Countable::class) ) );
Custom assert
To create a custom rule, implement the StructuraPhp\StructuraContracts\ExprInterface interface:
<?php final readonly class CustomRule implements ExprInterface { public function __construct( private string $message = '', ) {} public function __toString(): string { return 'exemple'; // Name of the rule } public function assert(ClassDescription $class): bool { return true; // Must return false if the test fails } public function getViolation(ClassDescription $class): ViolationValueObject { return new ViolationValueObject( 'error message', // Console output $this::class, $class->lines, $class->getFileBasename(), $this->message, ); } }
To use a custom rule, add it using the addExpr()
method.
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should(fn(Expr $expr) => $expr ->addExpr(new CustomRule('foo')) );
Use existing rules as an example.
With PHPUnit
Structura can integrate architecture testing with PHPUnit with this project: