jeckel-lab/contract

Contract / Interfaces used by other packages and DDD projects

v2.5.1 2024-03-25 15:09 UTC

This package is auto-updated.

Last update: 2024-04-25 15:23:31 UTC


README

Latest Stable Version Total Downloads Build Status codecov Mutation testing badge

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:

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.