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-09-23 21:51:17 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); } }
toBeClasses() and allScripts()
There are two types of analysis:
// Analysis of classes. A class MUST be present, otherwise an exception is raised. $this->allClasses() // Analysis of all PHP scripts. $this->allScripts()
If you choose script analysis, all PHP code can be analysed, but only rules can be used.
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:
// with allClasses() ->that(static fn(Expr $expr): Expr => $expr->toBeClasses()) // with allScript() ->that(static fn(ExprScript $expr): ExprScript => $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:
// with allClasses() ->should(static fn(Expr $expr): Expr => $expr ->toBeFinal() ->toBeReadonly() ->toHaveSuffix('Dto') ->toExtendsNothing() ->toHaveMethod('fromArray') ->toImplement(\JsonSerializable::class) ) // with allScript() ->should(static fn(ExprScript $expr): ExprScript => $expr)
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()
Must be a valid Unit Enum or Backed Enum.
$this ->allClasses() ->fromRaw('<?php enum Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeEnums(), );
toBeBackedEnums()
Must be a backed enumeration, if ScalarType
is not specified, int
and string
are accepted.
https://www.php.net/manual/en/language.enumerations.backed.php
use StructuraPhp\Structura\Enums\ScalarType; $this ->allClasses() ->fromRaw('<?php enum Foo: string {}') ->should( static fn (Expr $assert): Expr => $assert->toBeBackedEnums(ScalarType::String), );
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(), );
toBeAttribute()
- Must be a syntax-compliant attribute,
- Must be instantiable by a class reflection,
- And uses valid flags.
$this ->allClasses() ->fromRaw('<?php #[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)] class Foo {}') ->should( static fn (Expr $assert): Expr => $assert->toBeAttribute(\Attribute::TARGET_CLASS_CONSTANT), );
<?php [\Attribute(\Attribute::TARGET_CLASS_CONSTANT)] // OK class Foo { } #[Custom] // KO class Bar { } (new ReflectionClass(Bar::class))->getAttributes()[0]->newInstance(); // Fatal error: Uncaught Error: Attribute class "Custom" not found
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
dependsOnFunction()
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnFunction( names: ['strtolower', /* ... */], patterns: ['array_.+', /* ... */], ) );
You can use regexes to select dependencies
toNotDependsOnFunction()
Prohibit the use of function.
$this ->allClasses() ->should(fn(Expr $expr) => $expr ->dependsOnlyOnFunction( names: ['goto', /* ... */], patterns: ['.+exec', /* ... */], ) );
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));
toHaveAttribute()
$this ->allClasses() ->fromRaw('<?php #[\Deprecated] class Foo {}') ->should(fn(Expr $expr) => $expr->toHaveAttribute(Deprecated::class));
toHaveNoAttribute()
$this ->allClasses() ->fromRaw('<?php class Foo {}') ->should(fn(Expr $expr) => $expr->toHaveNoAttribute());
toHaveOnlyAttribute()
$this ->allClasses() ->fromRaw('<?php #[\Deprecated] class Foo {}') ->should(fn(Expr $expr) => $expr->toHaveOnlyAttribute(Deprecated::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'));
toHaveCorresponding()
Check the correspondence between a class/enum/interface/trait and a mask. To build the mask, you have access to the description of the current class.
Correspondence rules can be used in many scenarios, such as:
- If a model has a repository interface,
- If a model has a policy with the same name,
- If your controllers have associated queries or resources,
- ...
For example, you can check whether each unit test class has a corresponding class in your project :
$this ->allClasses() ->fromDir('tests/Unit') ->should( static fn(Expr $assert): Expr => $assert ->toHaveCorrespondingClass( static fn (ClassDescription $classDescription): string => preg_replace( '/^(.+?)\\\Tests\\\Unit\\\(.+?)(Test)$/', '$1\\\$2', $classDescription->namespace, ) ), );
toHaveCorrespondingClass()
Similar to toHaveCorresponding, but for matching with a class.
toHaveCorrespondingEnum()
Similar to toHaveCorresponding, but for matching with an enum.
toHaveCorrespondingInterface()
Similar to toHaveCorresponding, but for matching with an interface.
toHaveCorrespondingTrait()
Similar to toHaveCorresponding, but for matching with a trait.
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'));
toBeInOneOfTheNamespaces()
Allows you to specifically target classes contained in a namespace.
Information !
Anonymous classes cannot have namespaces
$this ->allClasses() ->fromDir('tests') ->that( fn(Expr $expr) => $expr ->toBeInOneOfTheNamespaces('Tests\Unit.+') ) ->should(fn(Expr $expr) => $expr /* our rules */);
You can use regexes to select namespaces.
notToBeInOneOfTheNamespaces()
Allows you to specifically target classes not contained in a namespace.
Information !
Anonymous classes cannot have namespaces
$this ->allClasses() ->fromDir('tests') ->that( fn(Expr $expr) => $expr ->notToBeInOneOfTheNamespaces('Tests\Unit.+') ) ->should(fn(Expr $expr) => $expr /* our rules */);
You can use regexes to select namespaces.
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 :
- for class analysis, implement the
StructuraPhp\Structura\Contracts\ExprInterface
interface - for script analysis, implement the
StructuraPhp\Structura\Contracts\ExprScriptInterface
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: