Abstracting the Doctrine ORM flush

In the previous article, we learned that we need to think in collections. We should not flush inside our repositories. Instead, we flush after a business transaction or use case is fully executed.
But one question remains. How can we abstract this?

This is indeed a very important question to ask. We should always abstract our repositories by creating an interface. But we also need to abstract committing to the Unit of Work. This because a Doctrine repository is only one possible implementation. Each might have its own distinct Unit of Work.

It makes no sense to flush the Doctrine Entity Manager when we are not using Doctrine. Doing so is a sign of coupling. Something we should avoid if possible.

Transaction demarcation

Doctrine uses a concept called transaction demarcation. This is the simple task of defining transaction boundaries. Defining the start and end of a transaction.

Instead of executing every single INSERT, UPDATE and DELETE; whenever it is called in the code. It is queued up until a flush is executed on the Unit of Work. This Unit of Work then wraps up all of these queries in a single transaction and executes it against the database.

This helps improve the performance of our applications. Every time we make a change against the database, this creates overhead. By queuing up changes we might need fewer queries and calls to the database.

This also means that the Unit of Work will keep track of all the changes in all tracked and persisted entities. Only on a flush it will compare the changes against the database and execute those.

There are 2 ways of dealing with transaction in Doctrine.

Implicitly Transaction

The first approach is the implicit transaction. Doctrine will always use transactions. But in this case, we as a developer do not explicitly define the transaction demarcation. As you can see in the following example:

$product = new Product('Product Name');
$this->entityManager->persist($product);
$this->entityManager->flush();

On flush, Doctrine will execute all the changes that the Unit of Work has tracked so far. This is also one of the reasons why it is dangerous to just call flush everywhere.

Implicitly flushing will commit all the changes queued up in the Unit of Work so far. Not just the entity you just persisted.

Explicitly Transaction

Another approach is to define the transaction boundaries yourself. You can do this by directly using the Doctrine DBAL connection.

$this->entityManager->getConnection()->beginTransaction(); // suspend auto-commit
try {
    $product = new Product('Product Name');
    $this->entityManager->persist($product);
    $this->entityManager->flush();
    $this->entityManager->getConnection()->commit();
 } catch (Exception $e) {
    $this->entityManager->getConnection()->rollBack();
    throw $e;
 }

As you can see. You now explicitly start and end a transaction. This means that you will only execute the changes between the beginning and committing of a transaction. You also have the flexibility to rollback whenever something fails.

There is also a shorter alternative that you could use instead:

$this->entityManager->transactional(function(EntityManagerInterface $entityManager) {
    $product = new Product('Product Name');
    $entityManager->persist($product);
});

Abstracting the Transaction

We now have 2 solutions. We can create an interface with an implementation that just calls flush, and thus commits all tracked changes. Or we can create an interface that defines the start and end of a transaction. This then results in only committing your explicit transaction.

Well, let’s create an interface that has both solutions. In practice, you can choose to support one or the other. It all depends on your use cases and preferences.

interface UnitOfWorkInterface
{
    public function commit(): void;

    public function commitTransactional(callable $operation);
}

As you can see. We will have a Unit Of Work interface that provides a commit and a transactional commit method. You can name this whatever you want.

Now let us create a Doctrine implementation of this.

final class DoctrineUnitOfWork implements UnitOfWorkInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;
    
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function commit(): void
    {
        $this->entityManager->flush();
    }

    public function commitTransactional(callable $operation)
    {
        return $this->entityManager->transactional($operation);
    }
}

We use flush in the commit method. An an explicit transaction in the commitTransactional method.

As an example. We will create a simple implementation of this. Instead of injecting the Entity Manager directly. We will be injection our Unit Of Work interface.

final class PurchaseOrderService
{
    /**
     * @var PurchaseOrderRepositoryInterface
     */
    private $purchaseOrderRepository;

    /**
     * @var UnitOfWorkInterface
     */
    private $unitOfWork;
    
    public function __construct(
        PurchaseOrderRepositoryInterface $purchaseOrderRepository,
        UnitOfWorkInterface $unitOfWork
    ) {
        $this->purchaseOrderRepository = $purchaseOrderRepository;
        $this->unitOfWork = $unitOfWork;
    }
    
    public function createNewPurchaseOrder(string $name): PurchaseOrder
    {
        $purchaseOrderId = PurchaseOrderId::fromString(Uuid::uuid4()->toString());
        $customerId = CustomerId::fromString(Uuid::uuid4()->toString());
        $purchaseOrder = PurchaseOrder::create($purchaseOrderId, $customerId, $name);
        $this->purchaseOrderRepository->save($purchaseOrder);
        $this->unitOfWork->commit();

        return $purchaseOrder;
    }
}

Another option is do it with a explicit transaction instead:

   public function createNewPurchaseOrder(string $name): PurchaseOrder
    {
        return $this->unitOfWork->commitTransactional(function () use ($name) {
            $purchaseOrderId = PurchaseOrderId::fromString(Uuid::uuid4()->toString());
            $customerId = CustomerId::fromString(Uuid::uuid4()->toString());
            $purchaseOrder = PurchaseOrder::create($purchaseOrderId, $customerId, $name);
            $this->purchaseOrderRepository->save($purchaseOrder);

            return $purchaseOrder;
        });
    }

At the end of the day, it is up to you to decide which solution you use. Both are valid options. You can test and decide what you and your team prefer.

Conclusion

It might be a good idea to abstract away the use of the Doctrine entity manager by creating a Unit of Work interface. This allows you to easily switch out Doctrine for another database solution. In that case, you only have to change your Unit Of Work and Repository implementations.

Note that this is not the magic solution for every situation. You might need to adapt to whatever use cases you need to support. Depending on how you work with batching and events, changes have to be made.

As always, think before you code. Learn and adapt. And most important. Do not get yourself dependent on libraries like Doctrine. Abstract and use interfaces whenever you can. You will thank your self later.