The core idea of this library is to get started with essential elements for a DDD implementation while keeping the dependencies as minimum as possible.
- Improve Specification pattern. When called from Application layer and targeting a query builder, the
getSpecExpression
method should be bound lately by or through the Repository.
src/
| Domain/
| Entity/
| Collection/
| Repository/
| Infrastructure/
| Helper/
| Identity/
There are two reasons to instantiate an entity, either for creation or reconstruction purpose.
Creating an entity occurs when the entity hasn't been saved yet and does not exist in the repository. When creating, the entity has no ID yet.
Reconstructing an entity occurs when the entity is retrieved from the repository and needs to be "rehydration".
An entity can be created with the constructor or with a static method create
. In the later case, the entity constructor should be private to avoid public usage.
The main advantage to a private constructor is to have complete freedom on constructor arguments.
It's suggested to create two static method: create
and reconstructe
. Usually, create
's method signature is the same as reconstructe
without the ID
argument. When it's possible, create
calls reconstructe
.
You must avoid to call Dispatcher
from your domain entities. Instead, consider declaring a trait and call it.
Doing so will considerably reduce coupling with Dispatcher
.
trait DomainEventDispatcher {
public function dispatch(\Ngirardet\PhpDdd\Domain\Event\IEvent $event) {
Publisher::instance()->dispatch($event);
}
}
Example of an entity dispatching an event:
class MyEntity {
use DomainEventDispatcher;
public function someMethod() {
$this->dispatch(new SomeEventTriggered());
}
}
Example of an event listener:
class MyEventListener implements IListener {
/**
* Some operations when the event is triggered
* @param \SomeEventTriggered|\IEvent $aDomainEvent
* @return void
*/
public function handle(SomeEventTriggered|IEvent $aDomainEvent): void {
$this->eventHasTriggered = true;
}
/**
* Check if this listener should interact with the queried event
*/
public function isSubscribedTo(IEvent $aDomainEvent): bool {
return get_class($aDomainEvent) === SomeEventTriggered::class;
}
/**
* Some getter to retrieve event or subject properties
* @return bool
*/
public function getEventHasTriggered(): bool {
return $this->eventHasTriggered;
}
}
Application Services are convenient to define some control logic inherent to the application layer. For instance, when your application applies rules before registering a user, such as is the username already registered. The service should receive a DTO, check rules, instantiates a new domain entity (either through the entity factory, if existing or with the entity constructor) and pass the entity to the repository.
If no service is needed, the domain entity should be instantiated through a factory. The factory can be either a static method of the entity class or a static method of a dedicated entity factory class.
Specification pattern is there to support collection or repository element filtering.
###Specification for collections
Create your own specification class by extending Ngirardet\PhpDdd\Common\Specification\BaseSpecification
and implement your own getSpecExpression
protected method.
class PriceSpecification extends \Ngirardet\PhpDdd\Common\Specification\BaseSpecification {
private float $maxPrice;
/**
* @param float $minPrice Required minimum inclusive price
* @param float|null $maxPrice Maximum inclusive price or equal $minPrice if null
**/
public function __construct(private float $minPrice, ?float $maxPrice = null) {
$this->maxPrice = $maxPrice ?? $this->minPrice; // $this->maxPrice will equal $this->minPrice if omitted
}
/**
* @param \Domain\Entity\Entity $element Element with a getPrice(): float method
*/
protected function getSpecExpression(mixed $element): callable {
return fn () => $element->getPrice() >= $this->minPrice && $element->getPrice() <= $this->maxPrice;
}
}
You'll need an abstract factory to properly interact with specifications for queries. The abstract factory class is an interface between the layer of the service (domain or application) and the infrastructure. It contains a list of methods the client will call when resolving a Specification.
// Example of a specification factory for the user root aggregate
namespace Domain\Model\User;
interface IUserSpecificationsFactory {
public function alreadyRegisteredUser(string $email, string $username);
}
The implementation of the abstract factory interface:
namespace Infrastructure\User\Specification;
class UserSpecificationsFactory implements \Domain\Model\User\IUserSpecificationsFactory {
public function alreadyRegisteredUser(string $email, string $username): \Ngirardet\PhpDdd\Common\Specification\ISpecification {
return new \Infrastructure\User\Specification\UserAlreadyRegisteredSpecification($email, $username);
}
}
And here's the specification for the ORM:
namespace Infrastructure\User\Specification;
class UserAlreadyRegisteredSpecification extends \Ngirardet\PhpDdd\Common\Specification\BaseSpecification {
public function __construct(private string $email, private string $username) {}
/**
* @param \Cake\ORM\Query $query
*/
protected function getSpecExpression(mixed $query): callable {
$query->where(
['OR' => [
'username' => $this->username,
'email' => $this->email
]]
);
return fn () => true; // BaseSpecification expects a callable
}
}
Lastly, we inject the dependency in the service:
class UserService {
public function __construct(private IUserRepository $userRepository, private IUserSpecificationFactory $specificationFactory) {}
...
public function register(string $email, string $username) {
$this->userRepository->find($this->specificationFactory->alreadyRegisteredUser($email, $username));
}
}
// Usage
class UsersController {
public function register() {
...
$repository = new \Infrastructure\User\UserRepository();
$specificationsFactory = new \Infrastructure\User\UserSpecificationsFactory();
$service = new \Application\User\UserService($repository, $specificationsFactory);
$service->register($request->email, $request->username);
...
}
}
Here is an example in common use case like finding specific records matching conditions.
/**
* Some repository class implementing the repository interface methods.
**/
class SomeRepository extends ORMBaseRepository implements SomeRepositoryInterface {
public function find(ISpecification $specification): self {
return $this->satisfiedBy(static fn ($queryBuilder) => $specification->isSatisfiedBy($queryBuilder));
}
}
/**
* Some ORM repository implementing the necessary methods to run queries
*/
class ORMBaseRepository implements \Ngirardet\PhpDdd\Domain\Repository\IRepository {
/**
* Method to find specific records based on specifications
* @param callable $callbackFilter
* @return $this
*/
protected function isSatisfiedBy(callable $callbackFilter): static {
$cloned = clone $this;
$callabackFilter($cloned->queryBuilder);
return $cloned;
}
}
// Custom specification
$repository->find(new StateSpecification(State::DISABLED()));
// OrSpecification, matches at least one condition
$repository->find(new OrSpecification(new PriceSpecification(50.5), new StateSpecification(State::DISABLED())));
// AndSpecification, matches all the conditions
$repository->find(new AndSpecification(new PriceSpecification(50.5), new StateSpecification(State::ACTIVE())));
Value Objects (aka Value Types) are Enumerators. They are perfect to define entity state or values that don't need an ID.
To create a value object by adding Ngirardet\PhpDdd\Common\BaseEnum
trait.
Declare private constants and there related public static method.
You may want to consider extending \Ngirardet\PhpDdd\Domain\Entity\ValueObject
class to implement isSameAs
method.
class MyValueObject extends \Ngirardet\PhpDdd\Domain\Entity\ValueObject {
use \Ngirardet\PhpDdd\Common\BaseEnum;
private const MY_CONST = 'Some string or integer value';
public static function MY_CONST(): self {
return self::constant(self::MY_CONST);
}
/**
* @param MyValueObject $compareTo
*
* @return bool
*/
public function isSameAs(ValueObject $compareTo): bool {
return $this === $compareTo;
}
}
// Usage
// Assuming we have an Entity class like this one
class Entity implements \Ngirardet\PhpDdd\Domain\Entity\IAggregateRoot {
public function __construct(private EntityId $id, private MyValueObject $valueObject) {}
}
// Instantiating an entity would look like
$id = new EntityId(...);
new Entity($id, MyValueObject::MY_CONST());
InMemoryRepository
abstract class simulates a repository engine, such as a DBMS, in memory. It's useful for running integration tests without needing a DB infrastructure.
To use InMemoryRepository, add php-ddd test namespace in your psr4 option composer.json autoload-dev.
{
"autoload-dev": {
"psr-4": {
"Ngirardet\\PhpDdd\\Test\\": "vendor/ngirardet/php-ddd/tests/"
}
}
}
Run the following command in console: composer dumpautoload
.
In your tests\Fixture\Infrastructure\Repository
folder, create a new class for your aggregate root entity. This class must extends Ngirardet\PhpDdd\Test\Fixture\Infrastructure\Repository\InMemoryRepository
and implements ...\Domain\YourAggregate\RepositoryInterface
.
YourAggregateRepositoryInterface
declares methods the repository must handle, i.e: save
, find
,...