DDD building blocks: ValueObject, Entity, AggregateRoot, Specification for SolidFrame

Maintainers

Package info

github.com/solidframe/ddd

pkg:composer/solidframe/ddd

Statistics

Installs: 9

Dependents: 1

Suggesters: 2

Stars: 0

v0.1.0 2026-04-11 20:42 UTC

This package is auto-updated.

Last update: 2026-04-19 11:25:05 UTC


README

Domain-Driven Design building blocks: Entity, ValueObject, AggregateRoot, and Specification pattern.

Framework-agnostic. Use with any PHP 8.2+ project.

Installation

composer require solidframe/ddd

Components

Entity

Entities have identity and are compared by identity, not by value.

use SolidFrame\Ddd\Entity\AbstractEntity;
use SolidFrame\Core\Identity\UuidIdentity;

final readonly class UserId extends UuidIdentity {}

final class User extends AbstractEntity
{
    private string $name;

    public function __construct(UserId $id, string $name)
    {
        parent::__construct($id);
        $this->name = $name;
    }

    public function rename(string $name): void
    {
        $this->name = $name;
    }

    public function name(): string
    {
        return $this->name;
    }
}

$user = new User(UserId::generate(), 'Kadir');
$user->identity(); // UserId instance
$user->equals($otherUser); // compares by identity

Aggregate Root

Aggregate roots are entities that record domain events.

use SolidFrame\Ddd\Aggregate\AbstractAggregateRoot;

final class Order extends AbstractAggregateRoot
{
    private OrderStatus $status;

    public static function place(OrderId $id, CustomerId $customerId): self
    {
        $order = new self($id);
        $order->status = OrderStatus::Placed;
        $order->recordThat(new OrderPlaced($id, $customerId));

        return $order;
    }

    public function cancel(): void
    {
        $this->status = OrderStatus::Cancelled;
        $this->recordThat(new OrderCancelled($this->identity()));
    }
}

$order = Order::place(OrderId::generate(), $customerId);
$events = $order->releaseEvents(); // [OrderPlaced]

Value Object

Immutable objects compared by value, not identity.

use SolidFrame\Ddd\ValueObject\StringValueObject;
use SolidFrame\Ddd\ValueObject\IntValueObject;
use SolidFrame\Ddd\ValueObject\BoolValueObject;

// String-based
final readonly class Email extends StringValueObject
{
    public static function from(string $value): static
    {
        filter_var($value, FILTER_VALIDATE_EMAIL) or throw new InvalidEmail($value);

        return parent::from($value);
    }
}

$email = Email::from('kadir@example.com');
$email->value();   // 'kadir@example.com'
$email->equals(Email::from('kadir@example.com')); // true
(string) $email;   // 'kadir@example.com'

// Integer-based
final readonly class Age extends IntValueObject
{
    public static function from(int $value): static
    {
        ($value >= 0) or throw InvalidAge::negative($value);

        return parent::from($value);
    }
}

// Boolean-based
final readonly class IsActive extends BoolValueObject {}

$active = IsActive::from(true);
$active->value(); // true

For composite value objects, implement ValueObjectInterface directly:

use SolidFrame\Ddd\ValueObject\ValueObjectInterface;

final readonly class Money implements ValueObjectInterface
{
    private function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public static function from(int $amount, string $currency): self
    {
        return new self($amount, $currency);
    }

    public function add(self $other): self
    {
        ($this->currency === $other->currency)
            or throw InvalidMoney::currencyMismatch($this->currency, $other->currency);

        return new self($this->amount + $other->amount, $this->currency);
    }

    public function equals(ValueObjectInterface $other): bool
    {
        return $other instanceof self
            && $this->amount === $other->amount
            && $this->currency === $other->currency;
    }

    public function __toString(): string
    {
        return sprintf('%d %s', $this->amount, $this->currency);
    }
}

Specification

Composable business rules using the Specification pattern.

use SolidFrame\Ddd\Specification\AbstractSpecification;

final class IsAdult extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->age() >= 18;
    }
}

final class HasVerifiedEmail extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->isEmailVerified();
    }
}

// Compose specifications
$canPurchase = (new IsAdult())->and(new HasVerifiedEmail());
$canPurchase->isSatisfiedBy($user); // true/false

// Negate
$isMinor = (new IsAdult())->not();

// OR
$canAccess = (new IsAdult())->or(new HasParentalConsent());

API Reference

Class / Interface Purpose
EntityInterface Contract for entities with identity
AbstractEntity Base entity with identity and equality
AggregateRootInterface Entity that records domain events
AbstractAggregateRoot Base aggregate with recordThat() / releaseEvents()
ValueObjectInterface Contract for immutable value objects
StringValueObject Base for string value objects
IntValueObject Base for integer value objects
BoolValueObject Base for boolean value objects
SpecificationInterface Composable business rule contract
AbstractSpecification Base specification with and() / or() / not()
AndSpecification Combines two specs with AND
OrSpecification Combines two specs with OR
NotSpecification Negates a spec

Related Packages

Contributing

This repository is a read-only split of the solidframe/solidframe monorepo, auto-synced on every push to main. Issues, pull requests, and discussions belong in the monorepo.