Implementing Multi-Tenant with Symfony and Doctrine using Wrapper Class: Step-by-Step Guide

Setting up multi-tenant with Symfony and Doctrine isn’t trivial the first time you have to do it. I’ll walk through how to implement this architecture using Doctrine’s wrapper_class.

What is multi-tenant?

In a multi-tenant app, a single instance serves several clients with their data fully isolated. Each client only sees their own data and has no idea they share infrastructure. You cut operating costs (one app, not N), you simplify maintenance (fix one bug, fix it for everyone) and, if you go with separate databases, you get a level of data isolation that any auditor will appreciate.

How to approach the implementation

First of all, I left this GitHub repository with a basic example so you can see it in action, not just read about it.

To implement multi-tenant with Symfony and Doctrine I use the wrapper_class property provided by Doctrine. This property lets you extend the Doctrine connection and create a method to close the current connection and open a new connection to another database. In the config/packages/doctrine.yaml file I configure the connection and specify the wrapper class:

doctrine:
  dbal:
    default_connection: default

    connections:
      default:
        url: '%env(resolve:DATABASE_URL)%'
        wrapper_class: App\Doctrine\DynamicConnection

The DynamicConnection class will be in charge of changing the database and will look like this:

class DynamicConnection extends Doctrine\DBAL\Connection
{
    public function swapDatabase(string $urlConnection): void
    {
        $this->closePreviousConnection();
        $params = $this->prepareConnectionParameters($urlConnection);
        parent::__construct($params, $this->_driver, $this->_config, $this->_eventManager);
    }
   ...
}

In this class you can do any extra work before switching the connection, such as handling open transactions.

In the swapDatabase method I pass the full connection string and then extract the parameters I need.

The connection still has to switch based on some external parameter, like the client’s token. I do that with a Symfony “subscriber”:

class DynamicConnectionSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly array $databases,
    ) {
    }
    public function onKernelRequest(RequestEvent $event): void
    {
        $connection = $event->getRequest()->query->get('conn');
        /** @var DynamicConnection $databaseConnection */
        $databaseConnection = $this->entityManager->getConnection();
        $urlConnection = $this->databases[$connection] ?? null;
        if ($urlConnection) {
            $databaseConnection->swapDatabase($urlConnection);
        }
    }
}

In the example I use the “subscriber” to switch the connection based on a query parameter called conn. In production, though, the usual thing is to use the client’s token to determine which database to access and build the connection string from that client’s info.

Conclusions

Multi-tenant with wrapper_class works well and keeps client data isolated with very little extra code. It’s not the only approach — sometimes a single database with tenant_id is a better fit, or even one instance per client if the load justifies it. Pick based on your project’s requirements, not by inertia.

P.S.: If you have any questions, you can contact me by sending a DM on twitter.

Luis Miguel Martín
Luis Miguel Martín

CTO at LCApps. I write about Claude Code, MCP, AI agents and real-world software architecture.

💡

¿Te ha gustado este artículo?

Explore more articles on development, best practices and tooling.