Domain-Driven Design (DDD) in PHP

Domain-Driven Design (DDD) is an approach to software development that emphasizes collaboration between technical experts and domain experts to build a shared understanding of the business logic. When applied effectively, DDD can help break down complex domains and create scalable, maintainable applications.

What is Domain-Driven Design?

At the core of DDD is the idea that complex business logic should be at the center of the application. By breaking down a domain into specific components and defining a common language between developers and stakeholders, DDD ensures the software accurately reflects the business’s needs. The main components of DDD include:

  • Entities
  • Value Objects
  • Aggregates
  • Repositories
  • Services
  • Factories

Let`s break down each component in the context of PHP development.

Key Concepts of DDD in PHP

1. Entities

Entities represent objects that have a distinct identity throughout the system. Even if their attributes change, their identity remains the same. For example, a user in a system can change their email address, but their identity (represented by an ID) remains constant.

class User
{
    private string $id;
    private string $name;
    private string $email;

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

    public function changeEmail(string $newEmail): void
    {
        $this->email = $newEmail;
    }
}

In this case, the User class is an entity with an immutable id but mutable attributes like name and email.

2. Value Objects

Value objects, unlike entities, do not have an identity. They are defined by their attributes, and two value objects are considered equal if their properties are the same. A common example of a value object is a Money or Address.

class Money
{
    private float $amount;
    private string $currency;

    public function __construct(float $amount, string $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function equals(Money $money): bool
    {
        return $this->amount === $money->amount && $this->currency === $money->currency;
    }
}

In this example, the Money object is considered equal to another Money object if both the amount and currency are the same.

3. Aggregates

An aggregate is a cluster of related entities and value objects that are treated as a single unit. One entity within the aggregate is the aggregate root, and all interactions with the aggregate are done through this root.

For example, in an e-commerce system, an Order aggregate might consist of an Order entity and multiple OrderItem entities:

class Order
{
    private string $id;
    private array $items = [];

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    public function addItem(OrderItem $item): void
    {
        $this->items[] = $item;
    }

    public function getItems(): array
    {
        return $this->items;
    }
}

In this example, the Order is the aggregate root, and any interaction with OrderItem should go through Order.

4. Repositories

Repositories are responsible for persisting and retrieving aggregates. They abstract the data storage, allowing the application to remain unaware of the underlying database or storage mechanism.

For example, a UserRepository could look like this:

interface UserRepository
{
    public function save(User $user): void;
    public function findById(string $id): ?User;
}

By using repositories, we ensure that our application works with domain objects instead of dealing directly with the database.

5. Domain Services

Some logic doesn’t naturally fit into entities or value objects. This is where domain services come in. Domain services encapsulate business logic that spans across multiple entities or aggregates.

For example, if we need to convert one currency to another in a financial system, we can create a CurrencyConversionService:

class CurrencyConversionService
{
    private ExchangeRateProvider $exchangeRateProvider;

    public function __construct(ExchangeRateProvider $exchangeRateProvider)
    {
        $this->exchangeRateProvider = $exchangeRateProvider;
    }

    public function convert(Money $money, string $targetCurrency): Money
    {
        $rate = $this->exchangeRateProvider->getRate($money->getCurrency(), $targetCurrency);
        $convertedAmount = $money->getAmount() * $rate;

        return new Money($convertedAmount, $targetCurrency);
    }
}

Here, the service handles a domain-specific operation—currency conversion—by relying on external exchange rate data.

6. Factories

Factories are responsible for creating complex objects or aggregates. Rather than using constructors directly, factories allow for more complex creation logic.

For example, we can create an OrderFactory to handle the creation of an Order with multiple items:

class OrderFactory
{
    public function createOrder(array $orderData): Order
    {
        $order = new Order($orderData['id']);

        foreach ($orderData['items'] as $itemData) {
            $orderItem = new OrderItem($itemData['productId'], $itemData['quantity']);
            $order->addItem($orderItem);
        }

        return $order;
    }
}

The factory encapsulates the creation logic, making it easier to manage complex object instantiation.

Implementing DDD in a PHP Framework

While you can implement DDD principles in any PHP application, some frameworks make it easier by providing built-in tools. For example:

  • Symfony: Symfony’s service container and event dispatcher make it a good choice for building DDD-oriented applications. You can easily implement services, repositories, and event-based architectures.
  • Laravel: Laravel offers tools like Eloquent for entities and repositories, as well as support for service providers, making it suitable for DDD as well.

Domain-Driven Design (DDD) is a powerful methodology for managing complex business logic in PHP applications. By understanding and implementing the core components—entities, value objects, aggregates, repositories, services, and factories—you can create systems that are both scalable and maintainable. Whether you’re building a small application or a large enterprise system, DDD helps align your software design with real-world business needs.