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
- fidry/cpu-core-counter: ^1.3
- 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
- dev-main
- 0.8.0
- 0.7.12
- 0.7.11
- 0.7.10
- 0.7.9
- 0.7.8
- 0.7.7
- 0.7.6
- 0.7.5
- 0.7.4
- 0.7.3
- 0.7.2
- 0.7.1
- 0.7.0
- 0.6.17
- 0.6.16
- 0.6.15
- 0.6.14
- 0.6.13
- 0.6.12
- 0.6.11
- 0.6.10
- 0.6.9
- 0.6.8
- 0.6.7
- 0.6.6
- 0.6.5
- 0.6.4
- 0.6.3
- 0.6.2
- 0.6.1
- 0.6.0
- 0.5.5
- 0.5.4
- 0.5.3
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.5
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.5
- 0.3.4
- 0.3.3
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.5
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- 0.0.5
- 0.0.4
- 0.0.3
- 0.0.2
- 0.0.1
- dev-use-ltp
This package is auto-updated.
Last update: 2026-05-28 14:09:00 UTC
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.
- Start with ready-made presets for common architecture styles.
- Tune, override, or skip individual preset rules in native PHP code.
- Catch boundary violations before they quietly become conventions.
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 # PSR-12 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
This 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::PSR15(), Preset::MVC(), Preset::DDD())
Cache directory
StructArmed stores its 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\Class_\MustBeFinalRule; 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()) // 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( '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 a 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.
Use replaceRule() to swap a preset rule's configuration — it throws RuleNotFoundException if the key does not exist, so a typo is caught immediately. Use rule() to add new custom rules; it can also overwrite an existing key silently, without verifying that the target exists.
Layer resolution
Layers are resolved by file path — no attributes needed on classes:
src/Domain/ → 'Domain'
src/Application/ → 'Application'
src/Infrastructure/ → 'Infrastructure'
Layer patterns (namespace-based layers)
When your architecture is expressed through namespace conventions rather than directory structure, use layerPattern() to resolve layers by matching the fully-qualified class name against a regex:
return Architecture::define() ->layerPattern('API', '/^App\\\\API\\\\.*$/') ->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/') ->layerPattern('Router', '/^App\\\\Router\\\\.*$/');
An optional third argument excludes classes whose FQN matches a second regex, even when the first matches:
// HTTP layer: includes everything under App\HTTP\, except App\HTTP\URI ->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/', '/^App\\\\HTTP\\\\URI$/') ->layerPattern('URI', '/^App\\\\HTTP\\\\URI$/')
Declarative ruleset
Once layers are defined (via layer() or layerPattern()), declare which layers each layer is allowed to depend on. Any dependency that resolves to a layer not in the allowed list is a violation:
return Architecture::define() ->layerPattern('API', '/^App\\\\API\\\\.*$/') ->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/') ->layerPattern('Database', '/^App\\\\Database\\\\.*$/') ->ruleset([ 'API' => ['HTTP'], // API may only depend on HTTP 'HTTP' => ['Database'], // HTTP may only depend on Database 'Database' => [], // Database may not depend on any layer ]);
Layers absent from the ruleset keys are not checked. Dependencies on external (non-registered) classes are always allowed.
Same-layer dependencies are always allowed regardless of the ruleset.
Skipping class-level violations
When a specific class-to-class dependency is a known exception, suppress it without disabling the whole layer rule:
->skipClassViolation('App\\HTTP\\ResponseTrait', [ 'App\\Pager\\PagerInterface', ]) ->skipClassViolation('App\\Log\\ChromeLoggerHandler', 'App\\HTTP\\ResponseInterface')
The first argument is the fully-qualified violating class name; the second is one or more dependency FQNs to ignore for that class.
Excluding paths from ruleset checks only
Test files often cross layer boundaries by design. Use skipPathsForRuleset() to exclude paths from ruleset evaluation while still scanning them for all other rules (e.g. PSR-4 namespace checks):
return Architecture::define() ->withPresets(Preset::PSR4()) ->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/') ->layerPattern('Database', '/^App\\\\Database\\\\.*$/') ->ruleset(['HTTP' => [], 'Database' => ['HTTP']]) ->skipPathsForRuleset(['*tests*', '*fixtures*']);
This is different from skipPaths() / skipPath(), which exclude files from all analysis.
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());
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::PSR15( 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::PSR15() |
*Middleware classes must implement PSR-15 MiddlewareInterface; *Handler classes must implement PSR-15 RequestHandlerInterface; StructArmed also enforces matching Middleware/Handler suffixes for implementations of those interfaces |
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 # StructArmed runs in parallel by default # to disable parallel processing (e.g. when debugging worker issues), pass `--disable-parallel`: vendor/bin/structarmed analyse --disable-parallel
CLI version commands
# Print the installed StructArmed version
vendor/bin/structarmed --version
vendor/bin/structarmed -V
Tips
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'
Adopting existing projects
Fix reported violations where practical before reaching for a baseline. If the remaining findings are too large to resolve in one pass, generate a baseline to record the known violations:
vendor/bin/structarmed analyse --generate-baseline=structarmed-baseline.php
Then reference it from your config:
return Architecture::define() ->baseline('structarmed-baseline.php') ->withPreset(Preset::PSR4());
Running vendor/bin/structarmed analyse will now pass ✅:
vendor/bin/structarmed analyse
StructArmed {version} — Architecture Enforcement
=================================================
✅ No violations found. (0.01s)
Important
Baseline entries are matched against future analysis results, so existing violations stay quiet while new ones still fail the run. Treat the baseline as a migration aid for legacy findings, not a way to silence issues you can fix now.