---
title: "Implementación de Multi-Tenant con Symfony y Doctrine mediante Wrapper Class: Guía Paso a Paso"
description: "Aprende cómo implementar la arquitectura multi-tenant con Symfony y Doctrine. Descubre sus ventajas, la configuración de conexiones dinámicas y ejemplos prácticos para separar los datos de cada cliente de manera eficiente y segura."
pubDate: "2023-07-08"
category: "programacion"
language: "es"
tags: ["symfony", "doctrine", "multi-tenant", "php", "desarrollo", "wrapper", "tenant"]
---
Montar multi-tenant con Symfony y Doctrine no es trivial la primera vez que te toca hacerlo. Voy a ver cómo implementar esta arquitectura aprovechando el `wrapper_class` de Doctrine.

## ¿Qué es el multi-tenant?

En una aplicación multi-tenant, una sola instancia sirve a varios clientes con sus datos aislados. Cada cliente ve solo lo suyo, sin enterarse de que comparte infraestructura. Te ahorra costes de operación (una app, no N), te simplifica el mantenimiento (arreglas un bug y se arregla para todos) y, si lo montas con bases de datos separadas, te da un aislamiento de datos que a más de un auditor le va a gustar.

## Cómo enfocar la implementación

En primer lugar, os he dejado [este repositorio](https://github.com/lmmartinb/symfony-multi-tenant-example) en github con un ejemplo básico para que podáis verlo y no solo leer.

Para implementar el multi-tenant con Symfony y Doctrine uso la propiedad `wrapper_class` que proporciona Doctrine.
Esta propiedad permite extender la conexión de Doctrine y crear un método para cerrar la conexión actual y abrir
una nueva conexión a otra base de datos. En el archivo `config/packages/doctrine.yaml` configuro la conexión
y especifico la wrapper class:

```yaml
doctrine:
  dbal:
    default_connection: default

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

La clase `DynamicConnection` se encargará del cambio de base de datos y se verá así:

```php
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);
    }
   ...
}
```

En esta clase puedes hacer cualquier tratamiento adicional antes del cambio de conexión, como gestionar transacciones abiertas.

En el método `swapDatabase` paso la cadena de conexión completa y después extraigo los parámetros necesarios.

Falta cambiar la conexión en función de algún parámetro externo, como el token del cliente. Esto lo hago con un "subscriber" de Symfony:

```php
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);
        }
    }
}
```

En el ejemplo uso el "subscriber" para cambiar la conexión según un parámetro de la consulta llamado `conn`.
Lo habitual en producción es usar el token del cliente para determinar la base de datos a la que hay que acceder y modificar la cadena con la información de ese cliente.

## Conclusiones

Multi-tenant con `wrapper_class` funciona bien y mantiene los datos aislados con poco código añadido. No es el único enfoque — hay casos en los que una sola base de datos con `tenant_id` encaja mejor, o incluso una instancia por cliente si la carga lo justifica. Elige según los requisitos de tu proyecto, no por inercia.

PD: Si tenéis cualquier duda podéis poneros en contacto conmigo enviando un DM por [twitter](https://x.com/lm_martinbar).