jeckel-lab / contract
Contract / Interfaces used by other packages and DDD projects
Installs: 6 899
Dependents: 7
Suggesters: 0
Security: 0
Stars: 2
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: ^8.0
- ext-json: *
- psr/http-message: ^1.0
Requires (Dev)
- infection/infection: ^0.25
- maglnet/composer-require-checker: ^3||^4
- phpmd/phpmd: ^2.11
- phpro/grumphp: ^1.5
- phpstan/phpstan: ^1.2
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-latest
- squizlabs/php_codesniffer: ^3.6
- vimeo/psalm: ^4.12
Suggests
- vimeo/psalm: Using psalm with this 'contract' will enable immutability and strong typing verification
README
Jeckel-Lab Contract
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 |
Documentation for version 2.x (php >= 8.0)
Domain
Domain contract are part of DDD implementation suggestion, it's not required and is not linked to any frameworks.
Identity
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
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
Event are notification about what happened during a use case.
Event must be:
- immutable
DomainEventAware
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());
ValueObject
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
Core
To be completed
Command Dispatcher
To be completed
See detailed implementation proposal: jeckel-lab/command-dispatcher
Query Dispatcher
To be completed
See detailed implementation proposal: jeckel-lab/query-dispatcher
Exceptions
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.