solidframe/phpstan-rules

PHPStan rules for DDD, CQRS, and Event Sourcing architectural enforcement

Maintainers

Package info

github.com/solidframe/phpstan-rules

Type:phpstan-extension

pkg:composer/solidframe/phpstan-rules

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:15 UTC


README

PHPStan rules for DDD, CQRS, and Event Sourcing architectural enforcement.

Static analysis catches architectural violations before tests even run.

Installation

composer require solidframe/phpstan-rules --dev

The rules are auto-registered via PHPStan's extension mechanism. No manual configuration needed.

Rules

CQRS Rules

Command Handler Must Return Void

Command handlers perform side effects and must not return values.

// OK
final readonly class PlaceOrderHandler implements CommandHandler
{
    public function __invoke(PlaceOrder $command): void { /* ... */ }
}

// ERROR: Command handler must return void
final readonly class PlaceOrderHandler implements CommandHandler
{
    public function __invoke(PlaceOrder $command): Order { /* ... */ }
}

Query Handler Must Not Return Void

Query handlers must return data.

// OK
final readonly class GetOrderHandler implements QueryHandler
{
    public function __invoke(GetOrder $query): OrderDto { /* ... */ }
}

// ERROR: Query handler must return a value
final readonly class GetOrderHandler implements QueryHandler
{
    public function __invoke(GetOrder $query): void { /* ... */ }
}

Handler Must Be Invokable

Handlers must implement __invoke() and have only one public method (besides __construct).

// OK
final readonly class PlaceOrderHandler implements CommandHandler
{
    public function __invoke(PlaceOrder $command): void { /* ... */ }
}

// ERROR: Handler must implement __invoke()
final readonly class PlaceOrderHandler implements CommandHandler
{
    public function handle(PlaceOrder $command): void { /* ... */ }
}

// ERROR: Handler must have only one public method
final readonly class PlaceOrderHandler implements CommandHandler
{
    public function __invoke(PlaceOrder $command): void { /* ... */ }
    public function anotherMethod(): void { /* ... */ }
}

Messages Must Be Final Readonly

Commands and Queries must be declared as final readonly.

// OK
final readonly class PlaceOrder implements Command {}

// ERROR: Command must be final
class PlaceOrder implements Command {}

// ERROR: Command must be readonly
final class PlaceOrder implements Command {}

Messages Must Not Extend

Commands and Queries must not extend other classes. Use composition.

// OK
final readonly class PlaceOrder implements Command
{
    public function __construct(public string $orderId) {}
}

// ERROR: Commands/Queries must not extend other classes
final readonly class PlaceOrder extends BaseCommand implements Command {}

DDD Rules

Value Object Must Be Readonly

Value objects are immutable. The class must be declared as readonly.

// OK
final readonly class Email extends StringValueObject {}

// ERROR: Value object must be readonly
final class Email extends StringValueObject {}

No Direct Aggregate Construction

Aggregate roots must be created via static factory methods, not new.

// OK
$order = Order::place($orderId, $customerId);

// ERROR: Use a static factory method instead of new
$order = new Order($orderId);

Construction inside the aggregate class itself is allowed.

Event Sourcing Rules

Events Must Be Final Readonly

Domain events are immutable data structures.

// OK
final readonly class OrderPlaced implements DomainEventInterface {}

// ERROR: Event must be final and readonly
class OrderPlaced implements DomainEventInterface {}

Apply Method Must Exist

For every event recorded via recordThat(), a corresponding apply{EventName}() method must exist.

// OK
final class Order extends AbstractEventSourcedAggregateRoot
{
    public function place(): void
    {
        $this->recordThat(new OrderPlaced(/* ... */));
    }

    protected function applyOrderPlaced(OrderPlaced $event): void
    {
        // apply state change
    }
}

// ERROR: Missing method applyOrderPlaced
final class Order extends AbstractEventSourcedAggregateRoot
{
    public function place(): void
    {
        $this->recordThat(new OrderPlaced(/* ... */));
    }
    // no applyOrderPlaced method!
}

Configuration

Rules work out of the box with SolidFrame interfaces. To use with custom interfaces, override in your phpstan.neon:

parameters:
    solidframe:
        commandHandlerInterface: App\Cqrs\CommandHandler
        queryHandlerInterface: App\Cqrs\QueryHandler
        commandInterface: App\Cqrs\Command
        queryInterface: App\Cqrs\Query
        eventInterface: App\Event\DomainEvent
        valueObjectInterface: App\Ddd\ValueObject
        aggregateRootClass: App\Ddd\AggregateRoot

Rule Summary

Rule ID Area
Command handler returns void solidframe.commandHandlerMustReturnVoid CQRS
Query handler returns data solidframe.queryHandlerMustNotReturnVoid CQRS
Handler is invokable solidframe.handlerMustBeInvokable CQRS
Handler has single public method solidframe.handlerSinglePublicMethod CQRS
Command is final solidframe.commandMustBeFinal CQRS
Command is readonly solidframe.commandMustBeReadonly CQRS
Query is final solidframe.queryMustBeFinal CQRS
Query is readonly solidframe.queryMustBeReadonly CQRS
Message has no parent class solidframe.messageMustNotExtend CQRS
Value object is readonly solidframe.valueObjectMustBeReadonly DDD
No direct aggregate construction solidframe.noDirectAggregateConstruction DDD
Event is final solidframe.eventMustBeFinal Event Sourcing
Event is readonly solidframe.eventMustBeReadonly Event Sourcing
Apply method exists for recorded events solidframe.applyMethodMustExist Event Sourcing

Complementary: ArchTest vs PHPStan Rules

Concern ArchTest PHPStan Rules
Namespace dependencies doesNotDependOn()
Class structure (final, readonly) areFinal(), areReadonly() Message/VO/Event rules
Handler return types Command void, Query non-void
Handler conventions Invokable, single public method
Event apply methods applyMethodMustExist
Aggregate construction noDirectAggregateConstruction
Module isolation Modular preset

Use both for comprehensive architectural enforcement.

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.