phpmentors/domain-kata
A Domain-Driven Design library for PHP offering reusable building blocks—entities, value objects, identifiers, repositories, and domain services—to help you model rich, expressive domains with clean, framework-agnostic code.
phpmentors/domain-kata is not a framework or a runtime library — it is a kata (a practice scaffold): a set of interfaces and abstract base classes for the DDD building blocks (Entity, ValueObject, Repository, Specification, Service, Usecase, Context, Event, Module, InOut). It ships almost no behavior, so the first thing to internalize is that you are adopting a vocabulary and a set of contracts, not a tool.
v1.4.0 (2015-06-13) is the release you are most likely to land on via VCS — it adds InOutInterface, ServiceInterface, IdentifiableInterface, and EntityCollectionInterface::toArray(). It is also, still, the last release; nothing about the maintenance story has changed.
Minimal steps:
composer require phpmentors/domain-kata
If Packagist metadata is stale, pin it via VCS instead:
{
"repositories": [{ "type": "vcs", "url": "https://github.com/phpmentors-jp/domain-kata" }],
"require": { "phpmentors/domain-kata": "dev-master" }
}
Where to look first, in this order, inside vendor/phpmentors/domain-kata/src/PHPMentors/DomainKata/:
ValueObject/ValueObjectInterface.php and Entity/EntityInterface.php — these define the equals() contract that everything else leans on.Entity/Operation/IdentifiableInterface.php — new in 1.4.0; the sanctioned way to ask an object for its identifier, which is exactly what your equals() should compare.Repository/RepositoryInterface.php and Entity/EntityCollectionInterface.php — deliberately thin; know exactly how thin (and note toArray() on the collection) before you design around it.Usecase/UsecaseInterface.php plus InOut/InOutInterface.php — the run() entry point and the input/output object it is designed to accept.Specification/SpecificationInterface.php — isSatisfiedBy(); a predicate, not a query builder.Event/ — AbstractEvent, EventBusInterface, EventBusAwareInterface, listener contracts.Service/ServiceInterface.php — a marker, nothing more. Read it once, stop looking.First use case — model a single aggregate end to end, e.g. a User entity with an Email value object:
use PHPMentors\DomainKata\ValueObject\ValueObjectInterface;
use PHPMentors\DomainKata\Entity\EntityInterface;
final class Email implements ValueObjectInterface
{
private $value;
public function __construct($value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email');
}
$this->value = $value;
}
public function getValue() { return $this->value; }
public function equals(ValueObjectInterface $other) {
return $other instanceof self && $this->value === $other->value;
}
}
Then a UserRepositoryInterface extends RepositoryInterface, an in-memory implementation for tests, a RegisterUserUsecase implements UsecaseInterface whose run() wires them together, and — on 1.4.0 — a small RegisterUserInput implements InOutInterface carrying the raw input. That vertical slice is the whole point of the kata; do it once by hand and the rest of the package becomes obvious. Verify exact method signatures by opening the interface files; several are empty markers and some carry type hints from the PHP 5 era.
Value objects: construct-validate-freeze. Do validation in the constructor, expose a getValue() accessor for primitive extraction, and implement equals() with === on the scalar. Never add setters. Use static named constructors (Email::fromString()) when you have more than one valid input shape.
Entities: identity is the contract. Give the entity an identity value object, implement IdentifiableInterface::getId() to return it, and implement equals() by comparing identity only — never by comparing attributes. If you extend AbstractEntity, read its implementation first; if it compares with == you may want to override with === on the identity VO to avoid cross-class coercion surprises. IdentifiableInterface is worth adopting even on entities where you don't strictly need it: it gives your specifications and repositories a uniform way to key objects without reflection or instanceof chains.
InOut: a DTO that is also an entity. InOutInterface extends EntityInterface, so your input objects inherit the equals() obligation. Keep them dumb (input scalars in, output id/result out), implement equals() on identity of the request, and let UsecaseInterface::run(InOutInterface $inOut) mutate the output via setOutput(). A minimal implementation:
final class RegisterUserInput implements InOutInterface
{
private $input;
private $output;
public function __construct(array $input) { $this->input = $input; }
public function setInput($input) { $this->input = $input; }
public function getInput() { return $this->input; }
public function setOutput($output) { $this->output = $output; }
public function getOutput() { return $this->output; }
public function equals(EntityInterface $other) { return $this === $other; }
}
The win is that controllers never touch a usecase's return value convention: they build an input, call run(), and read getOutput(). That keeps the boundary stable when you migrate the usecase to a queued job later.
Repositories: interface in domain, adapter in infrastructure. The package's RepositoryInterface is a marker-plus-collection contract. Keep it collection-oriented (add, remove, findById, findAll) and put framework-specific querying in a separate adapter class that implements the interface:
final class DoctrineUserRepository implements UserRepositoryInterface { /* ... */ }
final class InMemoryUserRepository implements UserRepositoryInterface { /* ... */ }
The in-memory implementation is the pattern that makes the kata worthwhile — it lets you test usecases and specifications with zero database. If you return EntityCollectionInterface from findAll(), implement toArray() as a plain array_map over the items; Laravel's collect() interop is a one-liner from there.
Specifications: compose predicates. Build small isSatisfiedBy($candidate) classes and compose them with And/Or/Not wrappers. Use them for in-memory filtering and business-rule guarding ($spec->isSatisfiedBy($order) before a state transition). Do not try to make a specification generate SQL — the interface cannot express that, and forcing it produces leaky abstractions.
Usecases: one run() per transaction boundary. Treat UsecaseInterface::run() as the application service entry point. It should: load aggregate via repository → invoke domain logic → persist → publish events → write the result into the InOut's output. Keep controllers thin; they resolve the usecase from the container, build the input, and call run().
Services vs. usecases. ServiceInterface is a marker for the DDD-book "service" concept that isn't a usecase — cross-aggregate domain operations, e.g. a TransferFundsService that coordinates two Account aggregates. The distinction is documentation, not enforcement: use it to signal "this is domain coordination, not an application entry point" in code review.
Events: publish after persistence. Extend AbstractEvent, implement EventBusAwareInterface on aggregates or services that need to emit, and inject the bus in your service provider. Register listeners as plain classes. Since the bus is synchronous and in-process, order matters: persist first, then dispatch, so a failing listener cannot leave you with phantom domain state.
Modules: bounded-context grouping. Module/ModuleInterface and the module manager exist to bundle usecases and listeners per context. In a Laravel app, map one module to one service provider and one namespace (App\Sales\..., App\Billing\...) — that keeps cross-context imports visible in code review.
Laravel integration. There is no service provider in this package (a Symfony bundle exists separately). Wire it yourself:
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
$this->app->singleton(EventBusInterface::class, InMemoryEventBus::class);
$this->app->bind(RegisterUserUsecase::class, function ($app) {
return new RegisterUserUsecase(
$app->make(UserRepositoryInterface::class),
$app->make(EventBusInterface::class)
);
});
Bind usecases as transient (not singleton) so per-request state does not leak between jobs and HTTP requests — this matters more now that InOutInterface objects may hold per-call output.
NOASSERTION. GitHub could not auto-detect the license file. Read LICENSE in the repo root manually before shipping this in commercial code. Do not assume MIT because the ecosystem usually is.equals() is the whole design — and 1.4.0 widens its blast radius. InOutInterface extends EntityInterface, so every input/output object you write now also needs a sane equals(). Get this wrong and nothing else works. Entity equality = identity only. Value object equality = all fields with ===. Watch out for == inside the provided AbstractEntity/AbstractValueObject — verify and override if needed, because == on two identity objects of different classes can return true in edge cases.IdentifiableInterface is not wired into equals(). Nothing in the package uses it. Its value is entirely what you make of it: a uniform identity accessor for your own specifications, repositories and test assertions. Don't expect AbstractEntity to consult it on your behalf.EntityCollectionInterface::toArray() has no prescribed semantics. The interface only promises "a native PHP array representation". Decide once whether that's a list of entities or of scalar rows, write it in your domain docblock, and stick to it — mixed conventions here leak into JSON responses fast.ServiceInterface is explicitly documented (1.4.0, issue #8) as a marker, ContextInterface and parts of Module are taxonomy. Do not assume the package will catch a misuse — it will not.findBy, no pagination, no UnitOfWork, no identity map. If you need those, add them to your interface that extends the package one; don't patch vendor code.isSatisfiedBy() pure means it operates on loaded objects. For large tables, pair it with a separate query/finder that pushes the predicate into SQL, and keep the specification as the source of truth for the rule.composer dump-autoload -o and check whether it uses PSR-0; you may need to register PHPMentors\ => vendor/phpmentors/domain-kata/src/PHPMentors/ in your composer.json. New-in-1.4.0 classes live in new sub-namespaces (InOut\, Entity\Operation\), so stale autoloaders bite here first.__call, no lazy loading in the package itself. Use Xdebug + var_dump on the concrete aggregate; problems will almost always be in your adapter, not in the kata. When an InOut looks empty after run(), check you didn't clone or reconstruct it between dispatch and read.InOutInterface for every usecase boundary; (3) extend AbstractValueObject/AbstractEntity for default equals(); (4) implement IdentifiableInterface on aggregates; (5) provide your own EventBusInterface implementation; (6) subclass AbstractEvent for event base behavior. Do not fork the package — the interfaces are the asset, not the code.App\Domain\.... That gives you the DDD structure without inheriting a decade-old, unmaintained dependency. If you do install it, quarantine the dependency behind your own interfaces so a future replacement is a one-file change.How can I help you explore Laravel packages today?