boundwize/structarmed
StructArmed is a dev-only PHP architecture guard: define layers and dependency rules, start from presets (PSR-4/1/12, MVC, DDD), then tune or skip checks in PHP. Run it in CI to catch boundary violations before they become conventions.
Installation:
composer require --dev boundwize/structarmed
Add to composer.json under require-dev if not using global CLI.
Initialize Configuration:
vendor/bin/structarmed init --preset=mvc
This generates structarmed.php in your project root with a basic MVC preset.
First Analysis:
vendor/bin/structarmed analyse
Run this in your CI pipeline or pre-commit hook to enforce architecture rules.
Preset::PSR4() or Preset::MVC() for Laravel projects.structarmed.php to define layers (e.g., src/Application/, src/Domain/).--report=json).Enforce Layer Isolation in a Laravel App:
// structarmed.php
return Architecture::define()
->layer('Domain', 'src/Domain/')
->layer('Application', 'src/Application/')
->layer('Infrastructure', 'src/Infrastructure/')
->withPreset(Preset::DDD())
->ruleset([
'Domain' => [],
'Application' => ['Domain'],
'Infrastructure' => ['Domain', 'Application'],
]);
Run vendor/bin/structarmed analyse to catch cross-layer dependencies (e.g., Infrastructure calling Domain directly).
Iterative Enforcement:
Preset::PSR4() to validate autoload paths.Preset::PSR12() for visibility rules).--generate-baseline for legacy code, then refactor incrementally.Layer-Based Development:
structarmed.php to mirror your project’s architecture:
->layer('API', 'src/Http/Controllers/')
->layer('Services', 'src/Services/')
->ruleset([
'API' => ['Services'],
'Services' => ['Domain'],
]);
Services layer only depends on Domain entities.Custom Rules for Laravel-Specific Needs:
API layer) must not instantiate Eloquent models directly:
->rule('controllers_must_not_instantiate_models', new MayNotDependOnRule(
from: 'API',
to: 'Domain',
toClassNamePattern: '/^.*Model$/'
));
CI/CD Pipeline:
Add to .github/workflows/ci.yml:
- name: Enforce Architecture
run: vendor/bin/structarmed analyse --report=json
Fail the job if violations exist.
Pre-Commit Hook:
Use husky or laravel-pint hooks to run:
vendor/bin/structarmed analyse --disable-parallel
(Disable parallel for deterministic local runs.)
PHPUnit Integration:
Add to phpunit.xml:
<extensions>
<bootstrap class="Boundwize\StructArmed\PHPUnit\StructArmedExtension"/>
</extensions>
This blocks test execution if architecture rules are violated.
Laravel Service Providers:
Use structarmed.php to validate that AppServiceProvider only depends on Domain or Application layers:
->layer('Providers', 'app/Providers/')
->ruleset(['Providers' => ['Domain', 'Application']]);
False Positives with Namespaces:
layerPattern(), ensure regexes are precise to avoid misclassifying classes. Example:
// ❌ Misclassifies App\Tests\Unit\Http\Controllers\* as 'API'
->layerPattern('API', '/^App\\\\.*Controller$/')
// ✅ Correct: Explicit namespace
->layerPattern('API', '/^App\\\\Http\\\\Controllers\\\\.*$/')
Baseline Overuse:
Parallel Processing Quirks:
--disable-parallel if:
Rule Key Typos:
DddPreset::ENTITY_MUST_BE_FINAL), not strings. Typos are caught at runtime:
// ❌ Silent failure (no rule applied)
->skipRule('ddd.entity.must_be_final')
// ✅ Explicit (throws RuleNotFoundException)
->skipRule(DddPreset::ENTITY_MUST_BE_FINAL);
Path Skipping Scope:
skipPaths() excludes files from all rules, while skipPathsForRuleset() excludes only ruleset checks. Example:
// ✅ Tests are scanned for PSR-12 but excluded from layer rules
->withPreset(Preset::PSR12())
->skipPathsForRuleset(['*tests*'])
Verbose Output:
Use --verbose to see layer resolution and rule application:
vendor/bin/structarmed analyse --verbose
Isolate Violations: Narrow down issues by path:
vendor/bin/structarmed analyse src/Http/Controllers/
Check Layer Resolution:
Add debug logs to structarmed.php:
->layer('API', 'src/Http/Controllers/')
->layerPattern('Services', '/^App\\\\Services\\\\.*$/', '/^App\\\\Services\\\\Tests\\\\.*$/');
Run with --verbose to confirm classes are classified correctly.
Custom Presets for Laravel:
Create a preset for Laravel-specific rules (e.g., Preset::Laravel()):
final class LaravelPreset implements PresetInterface {
public const CONTROLLERS_MUST_EXTEND_BASE_CONTROLLER = 'laravel.controllers_must_extend_base';
public function apply(Architecture $architecture): void {
$architecture
->layer('Controllers', 'app/Http/Controllers/')
->rule(
self::CONTROLLERS_MUST_EXTEND_BASE_CONTROLLER,
new MustExtendRule(
layer: 'Controllers',
baseClass: 'App\\Http\\Controllers\\Controller'
)
);
}
}
Register it in structarmed.php:
->withPreset(new LaravelPreset());
Dynamic Rule Configuration: Use Laravel’s config to parameterize rules:
// structarmed.php
$maxComplexity = config('structarmed.max_complexity', 5);
->withPreset(Preset::DDD(maxComplexity: $maxComplexity));
Event Listeners for Violations: Extend StructArmed’s event system (if supported in future versions) to log violations to a database or Slack.
Artisan Command Integration: Create a custom Artisan command to run StructArmed with Laravel’s config:
// app/Console/Commands/EnforceArchitecture.php
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class EnforceArchitecture extends Command {
protected $signature = 'arch:enforce';
public function handle() {
$process = new Process(['vendor/bin/structarmed', 'analyse']);
$process->run();
$this->output->write($process->getOutput());
if ($process->getExitCode() !== 0) {
$this->error('Architecture violations found!');
exit(1);
}
}
}
Register the command in AppServiceProvider:
public function boot() {
if ($this->app->runningInConsole()) {
$this->commands([EnforceArchitecture::class]);
}
}
Package Development: For Laravel packages, use StructArmed to enforce that:
src/ layer only depends on Domain or Application layers.src/Providers/) do not instantiate framework classes directly.Testing: Use the PHPUnit extension in your package tests to ensure architecture rules hold during development:
<!-- phpunit.xml.dist -->
<extensions>
<bootstrap class="Boundwize\StructArmed\PHPUnit\StructArmedExtension"/>
</extensions>
How can I help you explore Laravel packages today?