Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
PHPUnit Architecture Test
https://github.com/ta-tikoma/phpunit-architecture-test
Admin
PHPUnit Architecture Test is a tool that allows developers to write architecture tests alongside
...
Tokens:
4,364
Snippets:
23
Trust Score:
6.5
Update:
6 months ago
Context
Skills
Chat
Benchmark
90.9
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# PHPUnit Architecture Test PHPUnit Architecture Test is a PHP library that enables developers to write architecture tests alongside their feature and unit tests. It provides a fluent API for defining architectural layers, creating rules about dependencies between those layers, and enforcing code structure constraints. The library uses static code analysis to parse PHP projects and validate that actual dependencies match the intended architecture. The library integrates seamlessly with PHPUnit, allowing architecture tests to run as part of the standard test suite. By treating architecture as testable code, teams can prevent architectural drift, enforce layered patterns (like controllers → services → repositories), control method complexity, validate dependency injection patterns, and maintain property visibility rules. Tests fail immediately when code violates architectural constraints, providing rapid feedback during development. ## Installation and Setup Install the library via Composer and add the trait to your test base class. ```php // Install via composer // composer require --dev ta-tikoma/phpunit-architecture-test // Add trait to your test base class namespace Tests; use PHPUnit\Framework\TestCase as BaseTestCase; use PHPUnit\Architecture\ArchitectureAsserts; abstract class TestCase extends BaseTestCase { use ArchitectureAsserts; } ``` ## Creating Layers by Namespace Define architectural layers by filtering classes based on namespace patterns. ```php use Tests\TestCase; class ArchitectureTest extends TestCase { public function test_controllers_depend_on_services_not_repositories(): void { // Create layers by namespace prefix $controllers = $this->layer()->leaveByNameStart('App\\Controllers'); $services = $this->layer()->leaveByNameStart('App\\Services'); $repositories = $this->layer()->leaveByNameStart('App\\Repositories'); // Controllers must use services $this->assertDependOn($controllers, $services); // Controllers must not directly use repositories $this->assertDoesNotDependOn($controllers, $repositories); // Services must use repositories $this->assertDependOn($services, $repositories); } } ``` ## Filtering Layers by Path Filter classes based on their filesystem location. ```php public function test_app_code_does_not_depend_on_tests(): void { // Get all application code from src/ directory $app = $this->layer()->leaveByPathStart('/src'); // Get all test code from tests/ directory $tests = $this->layer()->leaveByPathStart('/tests'); // Application code must not depend on test code $this->assertDoesNotDependOn($app, $tests); // Tests can depend on application code (expected) $this->assertDependOn($tests, $app); } ``` ## Filtering Layers by Regular Expression Use regex patterns to define complex layer boundaries. ```php public function test_feature_modules_are_isolated(): void { // Match classes in Feature\Billing namespace $billing = $this->layer()->leaveByNameRegex('/^App\\\\Feature\\\\Billing\\\\/'); // Match classes in Feature\Reporting namespace $reporting = $this->layer()->leaveByNameRegex('/^App\\\\Feature\\\\Reporting\\\\/'); // Feature modules should not depend on each other $this->assertDoesNotDependOn($billing, $reporting); $this->assertDoesNotDependOn($reporting, $billing); } ``` ## Filtering Layers by Object Type Select classes, interfaces, traits, or enums specifically. ```php use PHPUnit\Architecture\Enums\ObjectType; public function test_interfaces_have_no_dependencies(): void { // Get all interfaces in the project $interfaces = $this->layer()->leaveByType(ObjectType::_INTERFACE); // Get all concrete classes $classes = $this->layer()->leaveByType(ObjectType::_CLASS); // Interfaces should not depend on concrete classes $this->assertDoesNotDependOn($interfaces, $classes); } public function test_traits_usage_pattern(): void { // Get all traits in the Concerns namespace $concerns = $this->layer() ->leaveByNameStart('App\\Concerns\\') ->leaveByType(ObjectType::_TRAIT); // Get domain models $models = $this->layer()->leaveByNameStart('App\\Models\\'); // Models can use concern traits $this->assertDependOn($models, $concerns); } ``` ## Excluding Classes from Layers Remove unwanted classes from layer definitions using exclusion filters. ```php public function test_services_except_legacy(): void { // Get all services except legacy ones $modernServices = $this->layer() ->leaveByNameStart('App\\Services\\') ->excludeByNameStart('App\\Services\\Legacy\\'); // Get repositories $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); // Modern services must use repositories $this->assertDependOn($modernServices, $repositories); } public function test_exclude_by_regex(): void { // Get all controllers except test/example ones $productionControllers = $this->layer() ->leaveByNameStart('App\\Controllers\\') ->excludeByNameRegex('/Test|Example/'); // Production controllers must not have Test/Example dependencies $testHelpers = $this->layer()->leaveByNameRegex('/Test|Example/'); $this->assertDoesNotDependOn($productionControllers, $testHelpers); } ``` ## Excluding Classes with Custom Closures Apply custom filtering logic using closures for fine-grained control. ```php public function test_exclude_with_closure(): void { // Exclude abstract classes from analysis $concreteClasses = $this->layer()->exclude( function ($objectDescription) { return str_contains($objectDescription->name, 'Abstract'); } ); // Or keep only specific classes $dtoClasses = $this->layer()->leave( function ($objectDescription) { return str_ends_with($objectDescription->name, 'DTO'); } ); } ``` ## Splitting Layers into Multiple Groups Dynamically create multiple layers from a single base layer using regex patterns. ```php public function test_feature_modules_do_not_cross_depend(): void { // Split all classes into feature-based layers // Classes like App\Feature\Billing\Service and App\Feature\Auth\Controller // will be grouped into separate 'Billing' and 'Auth' layers $featureLayers = $this->layer()->splitByNameRegex( '/^App\\\\Feature\\\\(?<layer>[^\\\\]+)/' ); // Each feature layer should not depend on other feature layers $this->assertDoesNotDependOn($featureLayers, $featureLayers); } public function test_split_domain_modules(): void { // Split domain modules into separate layers $domainLayers = $this->layer()->splitByNameRegex( '/^App\\\\Domain\\\\(?<layer>[^\\\\]+)/' ); // Optional: Each domain should be independent foreach ($domainLayers as $layerName => $layer) { // Custom validation per domain... } } ``` ## Splitting Layers with Custom Closure Split layers using custom logic beyond regex patterns. ```php public function test_split_by_custom_logic(): void { // Split by directory depth or any custom criteria $layers = $this->layer()->split( function ($objectDescription) { // Group by top-level namespace component $parts = explode('\\', $objectDescription->name); return $parts[1] ?? null; // Return layer name or null to exclude } ); // Now $layers is an array of Layer objects grouped by namespace } ``` ## Testing Method Argument Types Validate that method arguments come from expected architectural layers. ```php public function test_service_constructors_use_repositories(): void { // Get service classes $services = $this->layer()->leaveByNameStart('App\\Services\\'); // Get repository interfaces $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); // Service constructors must have repository arguments $this->assertIncomingsFrom($services, $repositories, ['__construct']); } public function test_controllers_do_not_inject_repositories(): void { $controllers = $this->layer()->leaveByNameStart('App\\Controllers\\'); $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); // Controllers must not have repository type-hints in any method $this->assertIncomingsNotFrom($controllers, $repositories); } ``` ## Testing Method Return Types Ensure method return types align with architectural boundaries. ```php public function test_repositories_return_domain_models(): void { $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); $models = $this->layer()->leaveByNameStart('App\\Models\\'); // Repository methods must return domain models $this->assertOutgoingFrom($repositories, $models); } public function test_controllers_return_responses(): void { $controllers = $this->layer()->leaveByNameStart('App\\Controllers\\'); $responses = $this->layer()->leaveByNameStart('Symfony\\Component\\HttpFoundation\\Response'); // Controller methods should return Response objects $this->assertOutgoingFrom($controllers, $responses); } ``` ## Limiting Method Complexity by Size Enforce maximum method size to maintain code quality. ```php public function test_service_methods_not_too_large(): void { $services = $this->layer()->leaveByNameStart('App\\Services\\'); // No method in services should exceed 30 lines $this->assertMethodSizeLessThan($services, 30); } public function test_controller_methods_are_thin(): void { $controllers = $this->layer()->leaveByNameStart('App\\Controllers\\'); // Controllers should be thin (max 20 lines per method) $this->assertMethodSizeLessThan($controllers, 20); } ``` ## Testing Property Visibility Enforce encapsulation by checking property visibility. ```php public function test_domain_models_have_no_public_properties(): void { $models = $this->layer()->leaveByNameStart('App\\Models\\'); // Domain models must not expose public properties // This enforces proper encapsulation $this->assertHasNotPublicProperties($models); } public function test_dtos_encapsulation(): void { $dtos = $this->layer()->leaveByNameRegex('/DTO$/'); // Even DTOs should not have public properties $this->assertHasNotPublicProperties($dtos); } ``` ## Extracting Layer Essence for Custom Assertions Extract specific attributes from all objects in a layer for custom validation. ```php public function test_all_properties_are_readonly(): void { $valueObjects = $this->layer()->leaveByNameStart('App\\ValueObjects\\'); // Extract all property visibility modifiers $visibilities = $valueObjects->essence('properties.*.visibility'); // Custom assertion: each must be private or protected $this->assertEach( $visibilities, fn($visibility) => in_array($visibility, ['private', 'protected']), fn($key, $visibility) => "Property $key has invalid visibility: $visibility" ); } public function test_extract_method_names(): void { $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); // Extract all method names $methodNames = $repositories->essence('methods.*.name'); // Verify naming conventions $this->assertEach( $methodNames, fn($name) => !str_starts_with($name, '_'), fn($key, $name) => "Method $name should not start with underscore" ); } ``` ## Custom Iterator Assertions - assertEach Validate that every item in a collection passes a condition. ```php public function test_each_controller_follows_naming(): void { $controllers = $this->layer()->leaveByNameStart('App\\Controllers\\'); $controllerNames = array_map( fn($obj) => $obj->name, iterator_to_array($controllers) ); // Each controller name must end with 'Controller' $this->assertEach( $controllerNames, fn($name) => str_ends_with($name, 'Controller'), fn($key, $name) => "Controller $name must end with 'Controller' suffix" ); } ``` ## Custom Iterator Assertions - assertNotOne Ensure no item in a collection matches a forbidden condition. ```php public function test_no_god_objects(): void { $allClasses = $this->layer()->leaveByType(ObjectType::_CLASS); $methodCounts = array_map( fn($obj) => ['name' => $obj->name, 'count' => count($obj->methods)], iterator_to_array($allClasses) ); // No class should have more than 20 methods (god object anti-pattern) $this->assertNotOne( $methodCounts, fn($data) => $data['count'] > 20, fn($key, $data) => "Class {$data['name']} has {$data['count']} methods (max 20)" ); } ``` ## Custom Iterator Assertions - assertAny Verify that at least one item in a collection matches a condition. ```php public function test_at_least_one_factory_exists(): void { $services = $this->layer()->leaveByNameStart('App\\Services\\'); $serviceNames = array_map( fn($obj) => $obj->name, iterator_to_array($services) ); // At least one factory service must exist $this->assertAny( $serviceNames, fn($name) => str_contains($name, 'Factory'), 'No factory service found in App\\Services namespace' ); } ``` ## Complex Multi-Layer Architecture Test Comprehensive example validating a complete layered architecture with multiple rules. ```php public function test_complete_layered_architecture(): void { // Define all architectural layers $controllers = $this->layer()->leaveByNameStart('App\\Http\\Controllers\\'); $services = $this->layer()->leaveByNameStart('App\\Services\\'); $repositories = $this->layer()->leaveByNameStart('App\\Repositories\\'); $models = $this->layer()->leaveByNameStart('App\\Models\\'); $dtos = $this->layer()->leaveByNameRegex('/DTO$/'); // Dependency rules: Controllers → Services → Repositories → Models $this->assertDependOn($controllers, $services); $this->assertDoesNotDependOn($controllers, $repositories); $this->assertDoesNotDependOn($controllers, $models); $this->assertDependOn($services, $repositories); $this->assertDependOn($repositories, $models); // Method complexity rules $this->assertMethodSizeLessThan($controllers, 20); $this->assertMethodSizeLessThan($services, 50); // Encapsulation rules $this->assertHasNotPublicProperties($models); $this->assertHasNotPublicProperties($services); // Type safety rules $this->assertIncomingsFrom($services, $repositories, ['__construct']); $this->assertOutgoingFrom($repositories, $models); // DTOs can be used everywhere $this->assertDependOn([$controllers, $services, $repositories], $dtos); } ``` ## Excluding Paths from Analysis Customize which paths are excluded from architecture analysis. ```php use Tests\TestCase; class CustomArchitectureTest extends TestCase { // Add custom excluded paths protected array $excludedPaths = [ 'src/Legacy', 'src/External', ]; public function test_modern_architecture(): void { // The layer() method will automatically exclude vendor/ // plus your custom excluded paths $app = $this->layer()->leaveByNameStart('App\\'); // Now $app excludes classes from src/Legacy and src/External $this->assertMethodSizeLessThan($app, 30); } } ``` ## Summary and Integration PHPUnit Architecture Test provides a comprehensive toolkit for enforcing architectural patterns through automated testing. The primary use cases include maintaining layered architectures (presentation → business logic → data access), preventing circular dependencies between modules, enforcing encapsulation through visibility rules, controlling code complexity through method size limits, and validating dependency injection patterns. By running architecture tests in CI/CD pipelines alongside unit and integration tests, teams catch architectural violations before they reach production. The library integrates naturally with existing PHPUnit test suites—simply add the `ArchitectureAsserts` trait to your test classes and create tests in your architecture test directory. The fluent API makes tests readable and maintainable, while the static analysis engine provides fast feedback without requiring a running application. Tests can start simple with basic dependency rules and evolve to comprehensive architectural validation as projects grow, making it an ideal tool for both greenfield projects establishing architectural boundaries and legacy projects undergoing refactoring.