Component Reference
All components live under the SeedWork\ namespace (Domain, Application, Infrastructure).
Domain layer
AggregateRoot (SeedWork\Domain\AggregateRoot)
- Role: Root of an aggregate; single entry point for changes; records domain events.
- Usage: Extend with your aggregate. Implement
validate(). State changes return a new instance and append events. Provide static factory methods (create(),build()). Annotate with@extends AggregateRoot<YourIdType>. - Key methods:
equals(AggregateRoot $other): bool,getDomainEvents(): array. $idtype: unconstrained (@template TId) — use any type your bounded context prefers: plainstring,int, a UUID library type, or a lightweight custom value object.
Entity (SeedWork\Domain\Entity)
- Role: Base for DDD entities. Identity over attributes; equality by ID.
- Usage: Extend per entity type; implement
validate(). Annotate with@extends Entity<YourIdType>. - Key methods:
equals(Entity $other): bool,validate(): void. $idtype: unconstrained (@template TId) — same freedom asAggregateRoot.
ValueObject (SeedWork\Domain\ValueObject)
- Role: Immutable object defined by attributes; equality by value.
- Usage: Extend; keep readonly and immutable. Implement
equals()andvalidate().
DomainEvent (SeedWork\Domain\DomainEvent)
- Role: Immutable record of something that happened (past tense, e.g.
MoneyDeposited). Carries a string id, the aggregate id that raised it, and a timestamp; event-specific facts are readonly properties of the subclass. - Usage: Extend; add your own readonly properties for domain-specific data. Use a static factory (e.g.
create()). The parent constructor requires three arguments in order:$id(unique string, e.g. a UUID),$aggregateId(identity of the raising aggregate), and$occurredAt(defaults to UTC now). Both$idand$aggregateIdmust be non-empty strings. - Key methods:
equals(DomainEvent $other): bool(by string id).
Repository (SeedWork\Domain\Repository)
- Role: Collection-like interface for an aggregate root: get by id, save, delete.
- Methods:
save(AggregateRoot $aggregateRoot): void,findById(mixed $id): ?AggregateRoot,deleteById(mixed $id): void.
UnitOfWork (SeedWork\Domain\UnitOfWork)
- Role: Transaction boundary: begin, commit, rollback.
- Methods:
createSession(): void,commit(): void,rollback(): void.
Exceptions
- DomainException (PHP stdlib
\DomainException): Base for domain errors. Extend to define concrete exceptions in your bounded context. No seedwork wrapper — consumers extend the stdlib class directly.
Application layer
Command (SeedWork\Application\Command)
- Role: Immutable DTO for a write use case. One class per use case.
- Usage: Extend; call
parent::__construct()so validation runs at instantiation. Overridevalidate(): voidto enforce field-level preconditions (no-op by default). An invalid command cannot be constructed —ValidationErrorsis thrown immediately.
CommandBus (SeedWork\Application\CommandBus)
- Role: Port to dispatch commands; one handler per command type.
- Methods:
dispatch(Command $command): Result.
CommandHandler (SeedWork\Application\CommandHandler)
- Role: Use case for a write. One handler per command.
- Usage: Implement
handle(Command $command): void. Orchestration only; no return value.
Result (SeedWork\Application\Result)
- Role: Outcome of a command dispatch. Either ok or failed with one or more errors.
- Factory methods:
Result::ok(): Result,Result::failed(non-empty-array<ResultError>): Result. - Methods:
isOk(): bool,isFailed(): bool,errors(): array<ResultError>.
ResultError (SeedWork\Application\ResultError)
- Role: A single error detail within a failed result.
- Properties:
string $code,string $description.
Query (SeedWork\Application\Query)
- Role: Immutable DTO for a read use case. No side effects.
- Usage: Extend; call
parent::__construct()so validation runs at instantiation. Overridevalidate(): voidto enforce field-level preconditions (no-op by default). An invalid query cannot be constructed —ValidationErrorsis thrown immediately.
QueryBus (SeedWork\Application\QueryBus)
- Role: Port to dispatch queries and return a result.
- Methods:
ask(Query $query): Maybe.
QueryHandler (SeedWork\Application\QueryHandler)
- Role: Use case for a read. Returns
Maybewrapping the result DTO. - Usage: Implement
handle(Query $query): Maybe. Read-only.
Maybe (SeedWork\Application\Maybe)
- Role: Represents an optional query result. Either a value (
just) or nothing. - Factory methods:
Maybe::just(mixed $value): Maybe(null not allowed),Maybe::nothing(): Maybe. - Methods:
hasValue(): bool,value(): mixed(throws if nothing).
DomainEventBusPublisher (SeedWork\Application\DomainEventBusPublisher)
- Role: Port to publish domain events from a repository decorator.
- Methods:
publish(array $events): void.
DomainEventBusSubscriber (SeedWork\Application\DomainEventBusSubscriber)
- Role: Port to register handlers in the composition root.
- Methods:
subscribe(string $eventType, DomainEventHandler $handler): void.
DomainEventBus (SeedWork\Application\DomainEventBus)
- Role: Full domain event bus contract: extends publisher and subscriber; adds lifecycle control.
- Methods: (inherits
publish()andsubscribe()) +dispatch(): void,discard(): void.
DomainEventHandler (SeedWork\Application\DomainEventHandler)
- Role: React to one event type. Registered via
subscribe($eventType, $handler). - Usage: Implement
handle(DomainEvent $event): void. One concern per handler; idempotent.
IntegrationEvent (SeedWork\Application\IntegrationEvent)
- Role: Contract for events published to external systems (eventual consistency via outbox).
- Properties:
id,type,version,aggregateId,occurredAt,payload,correlationId,causationId?,metadata?. - Generics:
@template TPayload of array<string, mixed> = array<string, mixed>. Subclasses narrowpayload's shape via@extends IntegrationEvent<array{...}>for PHPStan; this is a static-analysis aid only — seeAccountOpenedIntegrationEventin the fixture. Runtime type staysarray<string, mixed>.
IntegrationEventPublisher (SeedWork\Application\IntegrationEventPublisher)
- Role: Port to publish integration events. Implemented in Infrastructure (outbox or in-memory spy).
- Methods:
publish(array $events): void.
IntegrationEventHandler (SeedWork\Application\IntegrationEventHandler)
- Role: Handler for incoming integration events (entry-point in the subscriber service).
- Usage: Implement
handle(IntegrationEvent $event): void. One handler per event type.
BackgroundTask (SeedWork\Application\BackgroundTask)
- Role: DTO representing a background task to be scheduled for async execution.
- Properties:
id,type,payload,correlationId,causationId?,metadata?. - Generics:
@template TPayload of array<string, mixed> = array<string, mixed>. Subclasses narrowpayload's shape via@extends BackgroundTask<array{...}>for PHPStan; this is a static-analysis aid only — the runtime type staysarray<string, mixed>.
TaskScheduler (SeedWork\Application\TaskScheduler)
- Role: Port to schedule background tasks. Implemented in Infrastructure (outbox or in-memory spy).
- Methods:
schedule(BackgroundTask $task): void.
TaskHandler (SeedWork\Application\TaskHandler)
- Role: Handler for a specific background task type.
- Usage: Implement
handle(BackgroundTask $task): void. Registered by type inInMemoryTaskScheduler.
ValidationErrorDetail / ValidationErrors (SeedWork\Application\ValidationErrorDetail, SeedWork\Application\ValidationErrors)
- Role: Structured validation errors thrown by
validate()in Command/Query.
Infrastructure layer
RegistryCommandBus (SeedWork\Infrastructure\RegistryCommandBus)
- Role: In-process implementation of
CommandBus. Resolves handler by$command::class. - Usage:
register($commandFqcn, $handler), thendispatch($command).
RegistryQueryBus (SeedWork\Infrastructure\RegistryQueryBus)
- Role: In-process implementation of
QueryBus. Resolves handler by$query::class. - Usage:
register($queryFqcn, $handler), thenask($query).
CommandBusBuilder (SeedWork\Infrastructure\CommandBusBuilder)
- Role: Fluent builder for composing a
CommandBusdecorator pipeline. - Usage:
new CommandBusBuilder($registry), then chainwithTransaction($uow),withDomainEventCoordination($eventBus),use($closure), thenbuild(). The first step added becomes the outermost decorator. - Methods:
registry(): RegistryCommandBus,build(): CommandBus.
QueryBusBuilder (SeedWork\Infrastructure\QueryBusBuilder)
- Role: Fluent builder for composing a
QueryBusdecorator pipeline. - Usage:
new QueryBusBuilder($registry), then chainuse($closure), thenbuild(). - Methods:
registry(): RegistryQueryBus,build(): QueryBus.
TransactionalCommandBus (SeedWork\Infrastructure\TransactionalCommandBus)
- Role: Decorator that wraps each command in a
UnitOfWork(createSession → dispatch → commit or rollback). - Behaviour:
Result::ok()→commit(). Ifcommit()throws,rollback()then rethrow.Result::failed()→rollback(); the failedResultis returned, not thrown. Writes made before the domain rejection are never persisted.- Exception →
rollback()then rethrow.
DomainEventCoordinatorCommandBus (SeedWork\Infrastructure\DomainEventCoordinatorCommandBus)
- Role: Decorator that coordinates the
DomainEventBuslifecycle after each command. Result::ok()→eventBus->dispatch()(run buffered handlers).Result::failed()→eventBus->discard()(drop events).- Exception →
eventBus->discard()then rethrow (prevent stale events leaking). - Usage: Add via
CommandBusBuilder::withDomainEventCoordination($eventBus).
DeferredDomainEventBus (SeedWork\Infrastructure\DeferredDomainEventBus)
- Role: Buffers domain events on
publish(); dispatches them synchronously to subscribed handlers ondispatch(). Buffer is keyed byevent.id(idempotent per-transaction). - Usage: Subscribe handlers with
subscribe($eventFqcn, $handler). Pair withDomainEventCoordinatorCommandBusfor automatic lifecycle management. - Reentrancy guard:
publish(),dispatch(), anddiscard()each throw\LogicExceptionif called while adispatch()on the same instance is already in progress — e.g. a handler that, directly or indirectly, triggers another command dispatch that reaches the same bus instance before the outerdispatch()returns. This protects the single shared pending buffer from being read, cleared, or repopulated mid-iteration by a nested call. Sequential (non-nested) calls are unaffected; only genuine reentrancy is rejected. - Scoping: Instantiate per request/fiber, never as a container singleton, under async PHP runtimes (Swoole, OpenSwoole, RoadRunner, FrankenPHP worker mode, ReactPHP, AMPHP, native Fibers) — see "DomainEventBus Wiring" in
docs/coding-standards.mdfor why.
DomainEventPublishingRepository (SeedWork\Infrastructure\DomainEventPublishingRepository)
- Role: Repository decorator that publishes
$aggregate->getDomainEvents()viaDomainEventBusPublisherafter eachsave(). - Usage: Abstract — cannot be instantiated directly. Extend it and implement your domain repository interface so command handlers can be typed against the domain port:
// Infrastructure layer of your bounded context
final class PublishingBankAccountRepository
extends DomainEventPublishingRepository
implements BankAccountRepository
{
public function __construct(
BankAccountRepository $repository,
DomainEventBusPublisher $eventBus,
) {
parent::__construct($repository, $eventBus);
}
}
This is necessary because PHP's type system has no runtime generics: DomainEventPublishingRepository only implements the base Repository interface, so passing it where a domain-specific BankAccountRepository is expected would cause a TypeError. The typed subclass bridges the gap with three lines of code.
InMemoryRepository (SeedWork\Infrastructure\InMemoryRepository)
- Role: Base for in-memory repository test doubles. Non-final; extend to add query methods or pre-seed data.
- Usage: Extend per aggregate type.
IntegrationEventOutboxRecord / IntegrationEventOutboxRepository
- Role: Infrastructure outbox for integration events.
save()is idempotent (keyed byevent.id). - Status enum:
IntegrationEventOutboxStatus—Pending,Published,Failed. - Spy:
IntegrationEventOutboxRepositorySpywithall()andreset().
OutboxIntegrationEventPublisher (SeedWork\Infrastructure\OutboxIntegrationEventPublisher)
- Role: Implements
IntegrationEventPublishervia the outbox (persists toIntegrationEventOutboxRepository).
InMemoryIntegrationEventPublisher (SeedWork\Infrastructure\InMemoryIntegrationEventPublisher)
- Role: Spy implementation of
IntegrationEventPublisherfor tests. Captures events; does not execute them (integration events are for other bounded contexts). - Spy methods:
published(): array,reset().
TaskOutboxRecord / TaskOutboxRepository
- Role: Infrastructure outbox for background tasks.
save()is idempotent (keyed bytask.id). - Status enum:
TaskOutboxStatus—Pending,Delivered,Failed. - Spy:
TaskOutboxRepositorySpywithall()andreset().
OutboxTaskScheduler (SeedWork\Infrastructure\OutboxTaskScheduler)
- Role: Implements
TaskSchedulervia the outbox (persists toTaskOutboxRepository).
InMemoryTaskScheduler (SeedWork\Infrastructure\InMemoryTaskScheduler)
- Role: Spy + dispatcher implementation of
TaskSchedulerfor tests. Buffers tasks; executes them synchronously viaexecuteScheduled(). - Usage:
register($type, $handler)at setup; callexecuteScheduled()in functional tests to simulate the worker.reset()clears the scheduled list (handler registrations are preserved). - Spy methods:
scheduled(): array,reset().
Composition example
// Composition root (e.g. a service container or bootstrap file)
$repository = new InMemoryBankAccountRepository(); // implements BankAccountRepository
$domainBus = new DeferredDomainEventBus();
$domainBus->subscribe(AccountOpened::class, new AccountOpenedDomainEventHandler($integrationPublisher));
// Typed decorator: satisfies BankAccountRepository while adding event publishing
$publishingRepository = new PublishingBankAccountRepository($repository, $domainBus);
$registry = new RegistryCommandBus();
$registry->register(OpenAccountCommand::class, new OpenAccountCommandHandler($publishingRepository));
$registry->register(DepositMoneyCommand::class, new DepositMoneyCommandHandler($publishingRepository));
$commandBus = (new CommandBusBuilder($registry))
->withTransaction($unitOfWork)
->withDomainEventCoordination($domainBus)
->build();
// Entry point (controller, CLI, etc.)
$result = $commandBus->dispatch(new DepositMoneyCommand($accountId, 100, 'USD'));
if ($result->isFailed()) {
// handle domain rejection
}