solidframe/archtest

Architecture testing with fluent API: enforce DDD, CQRS, modular rules as PHPUnit tests for SolidFrame

Maintainers

Package info

github.com/solidframe/archtest

pkg:composer/solidframe/archtest

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

v0.1.0 2026-04-12 20:12 UTC

This package is auto-updated.

Last update: 2026-04-19 11:25:13 UTC


README

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.

Installation

composer require solidframe/archtest --dev

Quick Start

Fluent Assertions

use 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');
    }
}

Presets

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();
}

Available Assertions

Structural

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 enums

Naming

Arch::assertThat($dir)
    ->haveSuffix('Handler')     // class name ends with Handler
    ->havePrefix('Abstract');   // class name starts with Abstract

Inheritance

Arch::assertThat($dir)
    ->implement(DomainEventInterface::class)   // must implement interface
    ->extend(AbstractEntity::class);           // must extend class

Dependencies

Arch::assertThat($dir)
    ->doesNotDependOn('App\Infrastructure')   // no imports from namespace
    ->onlyDependsOn([                         // whitelist allowed namespaces
        'App\Domain',
        'SolidFrame\Core',
        'SolidFrame\Ddd',
    ]);

Preset Rules

DDD Preset

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

CQRS Preset

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)

Event-Driven Preset

Rule Description
Event immutability Events must be final readonly
Event interface Events must implement DomainEventInterface

Modular Preset

Rule Description
Module isolation Cross-module dependencies only allowed through Contract namespace

Custom Presets

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();

API Reference

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

Related Packages

Contributing

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.