boundwize / structarmed
Configurable PHP architecture guards — define your layers and rules, then keep them enforced
Fund package maintenance!
Requires
- php: ^8.2
- composer-runtime-api: ^2.0
- nikic/php-parser: ^5.7
Requires (Dev)
- laminas/laminas-coding-standard: ^3.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- rector/rector: dev-main
README
Configurable PHP architecture guards — define your layers and rules, then keep them enforced.
Turn architecture rules into executable checks
- Make architecture decisions executable, not just documented.
- Catch boundary violations before they quietly become conventions.
- Tune, override, or skip individual preset rules in native PHP code.
Installation
composer require --dev boundwize/structarmed
Quick start
# defaults to --preset=psr4 vendor/bin/structarmed init # verify source paths match composer.json PSR-4 mappings vendor/bin/structarmed init --preset=psr4 # enforce basic coding standard (tags, StudlyCaps, camelCase) vendor/bin/structarmed init --preset=psr1 # extends PSR-1: require explicit visibility on all members vendor/bin/structarmed init --preset=psr12 # thin controllers, model/view/service layer rules vendor/bin/structarmed init --preset=mvc # layer isolation, entity/VO/repository/event/service conventions vendor/bin/structarmed init --preset=ddd # enable all presets at once vendor/bin/structarmed init --preset=all
Generates a structarmed.php in your project root. Edit it to match your structure, then run:
vendor/bin/structarmed analyse
If violations are found, the output reports each one:
If everything passes, you get a clean summary:
Configuration
Default
<?php // structarmed.php use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\Preset; return Architecture::define() ->withPreset(Preset::PSR4());
Multiple presets
->withPresets(Preset::PSR4(), Preset::PSR1(), Preset::PSR12(), Preset::MVC(), Preset::DDD())
Cache directory
StructArmed stores analysis cache in the system temp directory by default. You can configure a project cache directory:
<?php use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\Preset; return Architecture::define() ->cacheDirectory('var/cache/structarmed') ->withPreset(Preset::PSR4());
Relative cache directories are resolved from the project root. --config also controls the cache directory used by analyse and --clear-cache.
Custom layers and rules
<?php use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\Preset; use Boundwize\StructArmed\Preset\Presets\DddPreset; use Boundwize\StructArmed\Rule\Rules\Layer\MayNotDependOnRule; use Boundwize\StructArmed\Rule\Rules\Method\MustHaveReturnTypeRule; return Architecture::define() ->layer('Domain', 'src/Domain/') ->layer('Application', 'src/Application/') ->layer('Infrastructure', 'src/Infrastructure/') ->skip([ 'tests/Fixtures/', 'var/cache/*', DddPreset::DOMAIN_NO_DATETIME, DddPreset::ENTITY_MUST_BE_FINAL => ['src/Legacy/'], ]) ->withPreset(Preset::DDD()) ->rule( 'domain.must_not_depend_on_infrastructure', new MayNotDependOnRule(from: 'Domain', to: 'Infrastructure', toPath: 'Infrastructure') ) ->rule( 'domain.public_methods_must_have_return_types', new MustHaveReturnTypeRule(layer: 'Domain') );
Inside skip(), string entries skip files or directories unless they match a registered rule key, keyed entries
skip paths for one specific rule, and rule key constants skip that rule entirely. You can also use
skipPath() / skipPaths() and skipRule() / skipRules() when you prefer the explicit methods.
Custom presets
A custom preset is a class that implements Boundwize\StructArmed\Preset\PresetInterface. Inside apply(), add the layers and
rules you want to reuse:
<?php use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\PresetInterface; use Boundwize\StructArmed\Rule\Rules\Method\MustHaveReturnTypeRule; final class MyPreset implements PresetInterface { public const METHODS_MUST_HAVE_RETURN_TYPES = 'source.methods_must_have_return_types'; public function apply(Architecture $architecture): void { $architecture ->layer('Source', 'src/') ->rule( self::METHODS_MUST_HAVE_RETURN_TYPES, new MustHaveReturnTypeRule(layer: 'Source') ); } }
Register it in structarmed.php:
<?php use App\Architecture\MyPreset; use Boundwize\StructArmed\Architecture; return Architecture::define() ->withPreset(new MyPreset());
Override preset rules
Use rule key constants — never raw strings:
<?php use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\Preset; use Boundwize\StructArmed\Preset\Presets\DddPreset; use Boundwize\StructArmed\Rule\Rules\Class_\MustBeFinalRule; use Boundwize\StructArmed\Rule\Rules\Class_\NamingConventionRule; return Architecture::define() ->layer('Domain', 'src/Domain/') ->layer('Application', 'src/Application/') ->layer('Infrastructure', 'src/Infrastructure/') ->withPreset(Preset::DDD()) // Replace a rule with a different configuration ->replaceRule( DddPreset::ENTITY_MUST_BE_FINAL, new MustBeFinalRule(layer: 'Domain', classNamePattern: '/Entity$|Aggregate$/') ) // Add your own custom rule ->rule( 'our.handlers.must_be_in_application', new NamingConventionRule( classNamePattern: '/Handler$/', mustBeInLayer: 'Application' ) );
Preset constructor parameters
->withPreset(Preset::DDD( maxComplexity: 3, // default: 5 maxMethodLength: 15, // default: 20 enforceFinalEntities: false, // default: true )) ->withPreset(Preset::MVC( controllerMaxComplexity: 3, // default: 5 controllerMaxDependencies: 4, // default: 5 viewMaxComplexity: 2, // default: 3 )) ->withPreset(Preset::PSR1( sourcePaths: ['src/', 'tests/'], // default: read composer.json PSR-4 paths )) ->withPreset(Preset::PSR12( sourcePaths: ['src/', 'tests/'], // default: read composer.json PSR-4 paths )) ->withPreset(Preset::PSR4( sourcePaths: ['src/', 'tests/'], // default: read composer.json PSR-4 paths ))
Available presets
| Preset | Rules |
|---|---|
Preset::PSR1() |
Basic Coding Standard checks: PHP tags, UTF-8 without BOM, symbols vs side effects, PSR-4 class placement, StudlyCaps class names, upper-case class constants, camelCase methods |
Preset::PSR12() |
Extends PSR-1: all methods, constants, and properties must declare explicit visibility |
Preset::PSR4() |
Verifies configured source paths exist in composer.json autoload or autoload-dev PSR-4 mappings |
Preset::DDD() |
Layer isolation, entity/VO/repository/event/service conventions |
Preset::MVC() |
Layer isolation, thin controllers, model/view/service rules |
PHPUnit extension
Run architecture checks as part of your test suite:
<!-- phpunit.xml --> <extensions> <bootstrap class="Boundwize\StructArmed\PHPUnit\StructArmedExtension"/> </extensions>
Violations cause the test run to fail before any tests execute.
CLI analyse commands
# Analyse with default config discovery vendor/bin/structarmed analyse vendor/bin/structarmed analyze # Analyse only specific paths vendor/bin/structarmed analyse src vendor/bin/structarmed analyze src tests # Custom config path vendor/bin/structarmed analyse --config=path/to/structarmed.php vendor/bin/structarmed analyze --config=path/to/structarmed.php # JSON output (for CI tools) vendor/bin/structarmed analyse --report=json vendor/bin/structarmed analyze --report=json
Layer resolution
Layers are resolved by file path — no attributes needed on classes:
src/Domain/ → 'Domain'
src/Application/ → 'Application'
src/Infrastructure/ → 'Infrastructure'
Rule key constants
Every preset rule has a public constant. Use constants, never raw strings:
// ✅ correct — caught by IDE and static analysis DddPreset::ENTITY_MUST_BE_FINAL // ❌ wrong — typo silently does nothing 'ddd.entity.must_be_fnal'