List of interfaces use as contract in other packages or DD projects
This contract includes some strong typings, object relation and psalm validation.
Require php >= 7.2.*
and php >= 8.0
Release name | Branch name | Php Version |
---|---|---|
1.x | release/1.X | php >= 7.2 & php <= 8.0 |
2.x | master | php >= 8.0 |
Domain contract are part of DDD implementation suggestion, it's not required and is not linked to any frameworks.
Identity are used to define a unique identifier for an Entity or a RootAggregate.
Identity must be:
- immutable
- final
- constructor should be private, use a factory method:
new
==> Generate (if possible) a new Identity object with a random value (like UUIDs)from
==> Instantiate Identity from an existing value
See detailed implementation proposal: jeckel-lab/identity-contract
Entity: main Entity contract
Entity must have an Id implementing the Identity
interface.
Don't forget to use @psalm templates
/**
* DiverId is using an `int` as unique identifier
* @implements Identity<int>
*/
final class DriverId implements Identity
{
}
/**
* Now Driver can use a DriverId as an identifier
* @implements Entity<DriverId>
*/
class Driver implements Entity
{
public function __construct(private DriverId $id)
{
}
/**
* @return DriverId
*/
public function getId(): Identity
{
return $id;
}
}
Event are notification about what happened during a use case.
Event must be:
- immutable
Entities and root aggregates handle domain events. To facilitate this behaviour, you can use this interface and trait:
This interface defines two methods:
/**
* @param Event ...$events
* @return static
*/
public function addDomainEvent(Event ...$events): static;
/**
* @return list<Event>
*/
public function popEvents(): array;
addDomainEvent
allow you to register new event occurred during a Use Case.popEvent
will empty the entity's event list at the end of a use case to dispatch them into an Event Dispatcher.
Just use the interface and trait into your entity:
class MyEntity implement DomainEventAwareInterface
{
use DomainEventAwareTrait;
/**
* Example of a use case that add an event to the queue
* @return self
*/
public function activateEntity(): self
{
$this->activated = true;
$this->addDomainEvent(new EntityActivated($this->id));
return $this;
}
//...
}
And if you use the CommandBus pattern, then you can add events to the response easily:
new CommandResponse(events: $entity->popEvents());
Using ValueObject
to embed a value (or group of value for complex types) as an object allow you:
- to use strong typing in the application (a
Speed
can not be mixed with any random float) - to embed data validation (be sure that the
Speed
is always a positive value, is lower than a reasonable value, etc.)
Value object must be defined as:
- immutable (one's instantiated, they should not be modified unless a new instance is created).
- final
- constructor should be private, use the static
from
method as a factory - when requesting to ValueObject with same value,
from
should return the same instance
Think about implementing it like this:
final class Speed implements ValueObject, ValueObjectFactory
{
private static $instances = [];
private function __constructor(private float $speed)
{
}
/**
* @param mixed $value
* @return static
* @throws InvalidArgumentException
*/
public static function from(mixed $speedValue): static
{
if (! self::$instances[$speedValue]) {
if ($speedValue < 0) {
throw new InvalidArgumentException('Speed needs to be positive');
}
self::$instances[$speedValue] = new self($speedValue);
}
self::$instances[$speedValue]
}
// implements other methods
}
// And now
$speed1 = Speed::from(85.2);
$speed2 = Speed::from(85.2);
$speed1 === $speed2; // is true
To be completed
To be completed
See detailed implementation proposal: jeckel-lab/command-dispatcher
To be completed
See detailed implementation proposal: jeckel-lab/query-dispatcher
Each layer has it's own Exception interface that extends Throwable
:
- Core: CoreException
- Domain: DomainException
- Infrastructure: InfrastructureException
- Presentation: PresentationException
In each layer, when we need to throw an Exception, we create a new class corresponding to the type of Exception. This class must:
- extends one of the SPL exception or another (more generic) exception from the same namespace.
- implements the exception interface of the current layer.