Architecture testing with fluent API: enforce DDD, CQRS, event-driven, and modular rules as PHPUnit tests.
Write architectural constraints as tests. Break a rule, break the build.
composer require solidframe/archtest --devuse SolidFrame\Archtest\Arch;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
final class ArchitectureTest extends TestCase
{
#[Test]
public function valueObjectsAreFinalAndReadonly(): void
{
Arch::assertThat(__DIR__ . '/../src/Domain/ValueObject')
->areFinal()
->areReadonly();
}
#[Test]
public function domainDoesNotDependOnInfrastructure(): void
{
Arch::assertThat(__DIR__ . '/../src/Domain')
->doesNotDependOn('App\Infrastructure');
}
#[Test]
public function handlersHaveCorrectSuffix(): void
{
Arch::assertThat(__DIR__ . '/../src/Application/Handler')
->haveSuffix('Handler');
}
}Built-in presets enforce common architectural patterns with zero configuration:
#[Test]
public function dddRules(): void
{
Arch::preset('ddd', [
'domainDir' => __DIR__ . '/../src/Domain',
'infrastructureDir' => __DIR__ . '/../src/Infrastructure',
'applicationDir' => __DIR__ . '/../src/Application',
])->assert();
}
#[Test]
public function cqrsRules(): void
{
Arch::preset('cqrs', [
'commandDir' => __DIR__ . '/../src/Application/Command',
'queryDir' => __DIR__ . '/../src/Application/Query',
'handlerDir' => __DIR__ . '/../src/Application/Handler',
])->assert();
}
#[Test]
public function eventDrivenRules(): void
{
Arch::preset('event-driven', [
'eventDir' => __DIR__ . '/../src/Domain/Event',
])->assert();
}
#[Test]
public function modularRules(): void
{
Arch::preset('modular', [
'modulesDir' => __DIR__ . '/../modules',
'contractSubNamespace' => 'Contract', // default
])->assert();
}Arch::assertThat($dir)
->areFinal() // all classes must be final
->areReadonly() // all classes must be readonly
->areAbstract() // all classes must be abstract
->areInterfaces() // all must be interfaces
->areEnums(); // all must be enumsArch::assertThat($dir)
->haveSuffix('Handler') // class name ends with Handler
->havePrefix('Abstract'); // class name starts with AbstractArch::assertThat($dir)
->implement(DomainEventInterface::class) // must implement interface
->extend(AbstractEntity::class); // must extend classArch::assertThat($dir)
->doesNotDependOn('App\Infrastructure') // no imports from namespace
->onlyDependsOn([ // whitelist allowed namespaces
'App\Domain',
'SolidFrame\Core',
'SolidFrame\Ddd',
]);| Rule | Description |
|---|---|
| Domain isolation | Domain must not depend on Infrastructure or Application |
| ValueObject final | All ValueObject classes must be final |
| ValueObject readonly | All ValueObject classes must be readonly |
| Rule | Description |
|---|---|
| Command immutability | Commands must be final readonly |
| Query immutability | Queries must be final readonly |
| Handler pairing | Each Command/Query must have a matching Handler (optional) |
| Rule | Description |
|---|---|
| Event immutability | Events must be final readonly |
| Event interface | Events must implement DomainEventInterface |
| Rule | Description |
|---|---|
| Module isolation | Cross-module dependencies only allowed through Contract namespace |
Implement PresetInterface for your own rules:
use SolidFrame\Archtest\Preset\PresetInterface;
final readonly class MyCustomPreset implements PresetInterface
{
public function __construct(private string $srcDir) {}
public function evaluate(): array
{
$violations = [];
// your validation logic...
// return array of violation message strings
return $violations;
}
}
// Usage
Arch::presetFrom(new MyCustomPreset(__DIR__ . '/../src'))->assert();| Class / Interface | Purpose |
|---|---|
Arch |
Main entry point: assertThat() and preset() |
ArchExpectation |
Fluent assertion builder |
PresetInterface |
Contract for custom presets |
PresetResult |
Wraps preset with assert() method |
DddPreset |
DDD rules (domain isolation, VO immutability) |
CqrsPreset |
CQRS rules (message immutability, handler pairing) |
EventDrivenPreset |
Event rules (immutability, interface) |
ModularPreset |
Module isolation rules |
ClassInfo |
Metadata about a PHP class |
ClassFinder |
Discovers classes in directories |
DependencyParser |
Extracts use-statement dependencies |
ArchViolationException |
Thrown on rule violations |
- solidframe/core — DomainEventInterface checked by presets
- solidframe/ddd — ValueObject, Entity conventions enforced
- solidframe/cqrs — Command/Query immutability enforced
- solidframe/phpstan-rules — Complementary static analysis rules
This repository is a read-only split of the solidframe/solidframe monorepo, auto-synced on every push to main. Issues, pull requests, and discussions belong in the monorepo.