One request, one Doctrine transaction

Whenever we create an API. We expect that one request is done in one step. This then means that one request, one call should also be one database transaction. If this is the case, then why should we manually start and end a transaction when writing our API calls? Can’t we just assume every request is one transaction, and then abstract this assumption as part of the infrastructure?

What we actually do end database transactions by adding flush at the end. But this means that our Presentation layer is aware of the Infrastructure. The presentation layer is now deciding when to end a transaction. And also aware that Doctrine is being used. In most cases, this is not really an issue. The presentation layer is dirty anyway, right?

Even if we do not care about layers, and that controllers are aware of flushing or ending transactions. Abstracting the request to infrastructure might be a good idea. It will allow us to be more consistent, write less code, and thus save time and reduce bugs. Let’s take a look at how we can accomplish this.

The Transaction Service Interface

To think in ‘one request, one transaction’. We will have to create a TransactionService interface.

interface TransactionServiceInterface
{
    public function start(): void;

    public function commit(): void;

    public function rollback(): void;
}

This interface is our contract. We need to be able to start, commit or rollback a transaction. Whatever ORM or database infrastructure we use. It needs to follow this simple contract.

And then, we need an implementation. As an example lets make one using Doctrine.

final class DoctrineTransactionService implements TransactionServiceInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * DoctrineTransactionService constructor.
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function start(): void
    {
        $this->entityManager->getConnection()->beginTransaction();
    }

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

    public function rollback(): void
    {
        if ($this->entityManager->getConnection()->isTransactionActive()) {
            $this->entityManager->getConnection()->rollBack();
        }
        $this->entityManager->clear();
    }
}

The idea is to start a transaction at the start of each request. And then to commit it at the end of the request. In case something goes wrong, we should rollback any changes.

So now, how do we do implement this in Symfony?

Request Transaction Subscriber

What we can do is use the Symfony kernel events. The CONTROLLER kernel event gets dispatched whenever a controller is found to handle the request. This seems to be the best moment to start a transaction.

It is of no use to start this at the beginning of a request. What if no controller is found to handle the request? Then we have started a transaction that is useless. The longer we can wait to start a transaction the better. But when a controller has been found to handle the request. This then means code will be going to be executed.

Next, when do we end the transaction? The best moment to do is when the controller has been successfully executed. And this is when a response is sent. This means we should commit our transaction at the RESPONSE kernel event.

But what should we do if an exception gets raised in the middle of the request?

That’s the perfect place to roll back the transaction. If something goes wrong, whatever database change happens, should be reverted and not executed.

Now let’s see how this translates into code.

final class RequestTransactionSubscriber implements EventSubscriberInterface
{
    /**
     * @var TransactionServiceInterface
     */
    private $transactionService;

    public function __construct(TransactionServiceInterface $transactionService)
    {
        $this->transactionService = $transactionService;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::CONTROLLER => ['startTransaction', 10],
            KernelEvents::RESPONSE => ['commitTransaction', 10],
            // In the case that both the Exception and Response events are triggered, we want to make sure the
            // transaction is rolled back before trying to commit it.
            KernelEvents::EXCEPTION => ['rollbackTransaction', 11],
        ];
    }

    public function startTransaction(): void
    {
        $this->transactionService->start();
    }

    public function commitTransaction(): void
    {
        $this->transactionService->commit();
    }

    public function rollbackTransaction(): void
    {
        $this->transactionService->rollback();
    }
}

Conclusion

As you can see. It is easy to remove any knowledge of Doctrine or transactions from your controllers. This by subscribing to the KernelEvents provided by Symfony. We are now forced to write our code knowing that everything needs to happen in one transaction. No more flushing in the middle of your requests. This smells bad anyway.

Remember, this is only for requests. If you are running queues or commands. You still have to manually start and end your transactions. Rules are different in those situations. If you need to process large amounts of data. You can not always do this in one transaction. You might need to flush in batches.

As always. Think before you code!