Implementación de Multi-Tenant con Symfony y Doctrine mediante Wrapper Class: Guía Paso a Paso

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 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:

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í:

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:

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.

Luis Miguel Martín
Luis Miguel Martín

CTO en LCApps. Escribo sobre Claude Code, MCP, agentes IA y arquitectura de software real.

💡

¿Te ha gustado este artículo?

Explora más artículos sobre desarrollo, buenas prácticas y herramientas.