Domain-Driven Design – Injecting Services on Entity Methods Calls

domain-driven-designdomain-modellazy-initializationobject-orientedPHP

Short format of question

Is it within best practices of DDD and OOP to inject services on entity method calls?

Long format example

Let's say we have the classic Order-LineItems case in DDD, where we have a Domain Entity called an Order, which also acts as the Aggregate Root, and that Entity is comprised not only of it's Value Objects, but also a collection of Line Item Entities.

Suppose we want fluent syntax in our application, so that we can do something like this (noting the syntax in line 2, where we call the getLineItems method):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

We don't want to inject any sort of LineItemRepository into the OrderEntity, as that is a violation of several principles I can think of. But, the fluency of the syntax is something that we really want, because it's easy to read and maintain, as well as test.

Consider the following code, noting the method getLineItems in OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Is that the accepted way of implementing fluent syntax in Entities without violating core principles of DDD and OOP? To me it seems fine, as we're only exposing the service layer, not the infrastructure layer (that is nested within the service)

Best Answer

It's totally fine to pass a Domain service in an entity call. Say, we need to calculate an invoice sum with some complicated algorithm that can depend on, say, a customer type. Here is what it might look like:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Another approach though is to separate out a business-logic that is located in domain service via domain events. Keep in mind that this approach implies just different application services, but the same database transaction scope.

The third approach is the one I'm in favor of: if I find myself using a domain service, that probably means I missed some domain concept, since I model my concepts primarily with nouns, not verbs. So, ideally, I don't need a domain service at all and a good part of all my business-logic resides in decorators.