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?.
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?.
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). - Note: Commits even on
Result::failed()— domain rejection is not an infrastructure error.
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.
DomainEventPublishingRepository (SeedWork\Infrastructure\DomainEventPublishingRepository)
- Role: Repository decorator that publishes
$aggregate->getDomainEvents()viaDomainEventBusPublisherafter eachsave(). - Usage: Do not instantiate 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
}