Cuando se lidia con aplicaciones symfony complejas, aplicaciones que vienen heredadas y han sido transformadas a symfony o tienen un propósito de multiservicio, entre otras razones, puede darse el caso de que tengamos en la misma base datos distintas tablas que realicen la misma misión, pero respondiendo a distinto nombre, a través de un prefijo o placeholder.

Cuando se realiza el mapeo de una entidad con la base de datos, se especifica el nombre de la tabla en la base de datos. Si queremos que la misma entidad pueda ser mapeada a distintas tablas deberiamos duplicar la entidad, cada una apuntando a su tabla en la base de datos. Esta sería una solución lógica si tuviesemos un número finito de tablas y tuviese sentido tener dos entidades distintas para relacionarlas. En otros casos en que la cantidad de tablas no está bien definida y limitada sino que puede variar según distintas variables externas como el entorno, sesión o petición request, puede tener solución igualmente.

Para los ejemplos vamos a suponer que tenemos la entidad MyEntityWithDynamicTableName que tiene como nombre de tabla table_placeholder_one. En este caso queremos modificar la parte de placeholder por otro valor según nos interese:

# Archivo: Acme/AppBundle/Resources/config/doctrine/MyEntityWithDynamicTableName.orm.yml
Acme\AppBundle\Entity\MyEntityWithDynamicTableName:
    type: entity
    table: table_placeholder_one

Solución con listener (No en tiempo de ejecución)

Si la apliación es genérica para varios clientes/sistemas o se despliega en distintos entornos, la mejor solución es modificar los metadatos de las relaciones en el momento que se generan. Estos metadatos se cachean y quedan guardados. Para ello debemos crear un listener al evento loadClassMetadata:

# Archivo: app/config/services.yml
services:
    mapping.listener:
        class: AppBundle\EventListener\TableNameListener
        tags:
            - { name: doctrine.event_listener, event: loadClassMetadata }

En este evento debemos modificar el nombre de la tabla (también vale para esquemas, índices, mapping, …).

use Doctrine\ORM\Event\LoadClassMetadataEventArgs;

class TableNameListener
{
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
    {
        $classMetadata = $eventArgs->getClassMetadata();
        $classMetadata->setTableName(str_replace('placeholder', 'xxx', $classMetadata->getTableName()));
    }
}

Según como tengas el sistema, en el más común de los casos estos metadatos van a estar cacheados, por lo que esta solución no sería válida en tiempo de ejecución.

Solución en tiempo de ejecución

Visto que la solución anterior no vale en muchas ocasiones, debemos realizar este reemplazo de nombre en tiempo de ejecución y no cuando se crea la caché. Esta misma solución anterior puede usarse sin usar el listener de loadClassMetadata y aplicar el cambio directamente antes de usar la entidad/repositorio:

public function indexAction()
{
    $entityManager = $this->getDoctrine()->getManager();
    $classMetadata = $entityManager->getClassMetadata('AppBundle:MyEntityWithDynamicTableName');
    $classMetadata->setTableName(str_replace('placeholder', 'xxx', $classMetadata->getTableName()));

    $repository = $entityManager->getRepository('AppBundle:MyEntityWithDynamicTableName'):
    
    $all = $repository->findAll();
}

Con esta solución la consulta SQL generada sería:

SELECT * FROM table_xxx_one

Aún así usar este extracto de código en todas la partes donde quiera modificarse el nombre de la tabla no es una buena práctica, la lógica va a estar repetida en todos los controladores y es mejor centralizarlo en un solo punto.

Solución con factoría + método con parámetros

Una posible implentación para mejorar el código anterior sería incluir la lógica de obtener el repositorio con los nuevos metadatos en una factoría abstracta, que devuelva el repositorio para la entidad deseada con el cambio de metadatos previamente establecido. De esta manera encapsulamos la lógica del cambio de nombre de tabla en un solo lugar.

# Archivo: Acme\AppBundle\Doctrine\GameRepositoryFactory.php
class DynamicTableRepositoryFactory
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $em)
    {
        $this->entityManager = $em;
    }

    public function getRepository($entity, $replacement)
    {
        $classMetadata = $this->entityManager->getClassMetadata($entity);
        $classMetadata->setTableName(str_replace('{placeholder}', $replacement, $classMetadata->getTableName()));

        return $this->entityManager->getRepository($entity);
    }
}

La definición del servicio sería la siguiente:

# Archivo: Acme\AppBundle\Resources\config\services.yml
services:
    acme.dynamic_table.repository.factory:
        class: Acme\AppBundle\Doctrine\DynamicTableRepositoryFactory
        arguments:
          - "@doctrine.orm.entity_manager"

De esta manera, el controlador anterior quedaría mucho más limpio, teniendo una invocación de este tipo:

public function indexAction()
{
    $repository = $this->get('acme.dynamic_table.repository.factory')
                        ->getRepository('AppBundle:MyEntityWithDynamicTableName', 'xxx');
    
    $all = $repository->findAll();
}

Esta solución es bastante válida si el reemplazo del placeholder (xxx en este caso) varía en la aplicación constantemente y puede esr distinto según el sitio en el que estemos. Si esto no es así, podemos pasar la siguiente solución.

Solución con servicio e inyección de datos

En caso de que el cambio al que correspondan todas las tablas sea el mismo (xxx en los ejemplos anteriores) se podría hacer que este parámetro se inyecte directamente en nuestro servicio de obtención del repositorio o en la factoría anterior. De esta manera podriamos obtener directamente el servicio con $this->get('app.my_entity_with_dynamic_table_name.repository') y que ya tuviese el placeholder cambiado con la nueva tabla o usar la factoría anterior pero sin pasar el segundo parámetro a getRepository.

Este caso puede darse en cuanto la variable viene por entorno, sesión o está definida por ejemplo en el parameters.yml.

Siguiendo el ejemplo de la factoría pero inyectando los nuevos parámetros, nuestra factoría pasaría a tomar el reemplazo en su constructor:

# Archivo: Acme\AppBundle\Doctrine\GameRepositoryFactory.php
class DynamicTableRepositoryFactory
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var string
     */
    private $remplacement;
    
    public function __construct(EntityManagerInterface $em, $replacement)
    {
        $this->entityManager = $em;
        $this->replacement = $replacement;
    }

    public function getRepository($entity)
    {
        $classMetadata = $this->entityManager->getClassMetadata($entity);
        $classMetadata->setTableName(str_replace('{placeholder}', $this->replacement, $classMetadata->getTableName()));

        return $this->entityManager->getRepository($entity);
    }
}

Y la definición del servicio tomaría como entrada el valor de reemplazo, en este ejemplo pasandole un valor definido en el parameters.yml

# Archivo: app/config/parameters.yml
parameters:
    app.replacement_table: xxx
# Archivo: Acme\AppBundle\Resources\config\services.yml
services:
    acme.dynamic_table.repository.factory:
        class: Acme\AppBundle\Doctrine\GameRepositoryFactory
        arguments:
          - "@doctrine.orm.entity_manager"
          - "%app.replacement_table%"

Otros ejemplos de reemplazo sería injectar la sesión (@session) para obtener un parámetro concreto o la petición request (@request_stack) entre otros.