Connexion dynamique à des bases de données avec Symfony et Doctrine

Dans le cadre de certains de nos projets, nous avons eu besoin d’utiliser des bases de données différentes, en fonction du domaine d’appel de l’api.
Ainsi, avec un même container déployé, nous pouvons switcher de base de données dynamiquement.

Tout d’abord, créons une Entité Site qui servira à stocker les noms des hosts ainsi que leur nom de BDD associé avec comme simples propriétés host et dbname.

#src\ConfigurationConnexion

<?php

namespace App\ConfigurationConnexion;

use App\Repository\SiteRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=SiteRepository::class)
 * @ORM\Table(name="site")
 *
 */
class Site
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var String
     *
     * @ORM\Column(type="string", length=255)
     */
    private $host;

    /**
     * @var String
     *
     * @ORM\Column(type="string", length=255)
     */
    private $dbName;

Maintenant, pour pouvoir stocker le site courant qui se connecte à notre API, il nous faut créer un SiteManager:

#src\ConfigurationConnexion

<?php

namespace App\ConfigurationEntity;

use App\Entity\Site;

/**
 * Class SiteManager
 * @package App\Site
 */
class SiteManager

{
    /**
     * @var Site
     */
    private $currentSite;

    /**
     * @return Site
     */

    public function getCurrentSite(): Site

    {
        return $this->currentSite;
    }

    /**
     * @param Site $currentSite
     * @return SiteManager
     */
    public function setCurrentSite(Site $currentSite): SiteManager

    {
        $this->currentSite = $currentSite;
        return $this;
    }

}

Dans le fichier .env paramètre deux adresses dns, une pour la connexion aux différentes BDD et une pour la connexion à la BDD de configuration, intégrant notre table Site créé au préalable.

#.env

DATABASE_URL="mysql://root:[email protected]:3306/bdd1?serverVersion=5.7"
DATABASE_CONFIG_URL="mysql://root:[email protected]:3306/configuration?serverVersion=5.7" 

Configurons doctrine pour lui permettre une connexion multiple à nos bases de données :

#config\doctrine.yaml

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                url: '%env(DATABASE_URL)%'
                wrapper_class: App\DBAL\SiteDbConnectionWrapper
            configuration:
                url: '%env(resolve:DATABASE_CONFIG_URL)%'

Dbal a un paramètre connections qui permet de définir plusieurs connexions avec leur url et d’autres paramètres de configurations.

Et un autre default_connection où l’on associera la connexion par défaut à la base de données.

Doctrine, avant la connexion à la base de données appellera la classe Connection pour envoyer les paramètres de connexion.
Pour pouvoir modifier les paramètres envoyés au driver pour la connexion, on va devoir passer par une classe dite ‘Wrapper’ qui étendra la classe Connection, qui sera appelé à chaque connexion à la base de données.

Voici notre classe SiteDbConnectionWrapper:

#src\DBAL

<?php

namespace App\DBAL;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Exception;

/**
 * Class SiteDbConnectionWrapper
 * @package App\DBAL
 */
class SiteDbConnectionWrapper extends Connection
{
    /**
     * @var array
     */
    private array $params;

    /**
     * SiteDbConnectionWrapper constructor.
     * @param array $params
     * @param Driver $driver
     * @param Configuration|null $config
     * @param EventManager|null $eventManager
     * @throws Exception
     */
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
        $this->params = $params;
        parent::__construct($params, $driver, $config, $eventManager);
    }

    /**
     * @param string $dbName
     * @throws Exception
     */
    public function selectDatabase(string $dbName)
    {
        if ($this->isConnected()) {
            $this->close();
        }
        unset($this->params['url']);
        $this->params['dbname'] = $dbName;

        parent::__construct(
            $this->params,
            $this->_driver,
            $this->_config,
            $this->_eventManager
        );
    }
}

Dans la méthode selectDatabaseon passera comme paramètre le nom de la base de données de l’host courant pour ainsi modifier le paramètre ‘dbname’ avec le nom de la BDD à se connecter.

Pour cela, nous devons créer un ‘EventListener’ qui va intercepter l’host qui se connecte à notre API, pour ensuite faire une requête sur l’entité Site pour récupérer le nom de la BDD suivant le nom de l’host.

#src\EventListener

<?php

namespace App\EventListener;

use App\Repository\SiteRepository;
use App\Site\SiteManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * Class CurrentSiteListener
 * @package App\EventListener
 */
class CurrentSiteListener
{
    /**
     * @var SiteManager
     */
    private SiteManager $siteManager;

    /**
     * @var SiteRepository
     */
    private SiteRepository $siteRepository;

    /** @var EntityManagerInterface */
    private EntityManagerInterface $manager;

    /**
     * @var CacheInterface
     */
    private CacheInterface $cacheConfig;

    /**
     * CurrentSiteListener constructor.
     * @param SiteManager $siteManager
     * @param SiteRepository $siteRepository
     * @param EntityManagerInterface $manager
     * @param CacheInterface $cacheConfig
     */
    public function __construct(
        SiteManager $siteManager,
        SiteRepository $siteRepository,
        EntityManagerInterface $manager,
        CacheInterface $cacheConfig
    ) {
        $this->siteManager = $siteManager;
        $this->siteRepository = $siteRepository;
        $this->manager = $manager;
        $this->cacheConfig = $cacheConfig;
    }

    /**
     * @param RequestEvent $event
     */
    public function onKernelRequest(RequestEvent $event)
    {
        $request = $event->getRequest();
        $currentHost = $request->getHost();
        
        $site = $this->siteRepository->findByHost($currentHost);

        if (!$site) {
            throw new NotFoundHttpException('no config found for this host ' . $currentHost);
        }

        $this->siteManager->setCurrentSite($site);
       $this->manager->getConnection()->selectDatabase($site->getDbName()); //$site['dbName']
    }
}

Ne pas oublier de déclarer le service CurrentSiteListener dans le fichier service.yaml :

   #config\service.yaml
   
   App\EventListener\CurrentSiteListener:
        tags:
            - { name: kernel.event_listener, event: kernel.request, priority: 2048 }

Doctrine a besoin de pouvoir se connecter à la base de données qui contient la table de Configuration, pour cela, nous allons avoir besoin de configurer cette connexion.

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                url: '%env(resolve:DATABASE_URL)%'
                wrapper_class: App\DBAL\SiteDbConnectionWrapper
            configuration:
                url: '%env(resolve:DATABASE_CONFIG_URL)%'
        override_url: true
        url: '%env(resolve:DATABASE_URL)%'
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                naming_strategy: doctrine.orm.naming_strategy.underscore
                mappings:
                    App:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity'
                        prefix: 'App\Entity'
                        alias: App
            configuration:
                connection: configuration
                naming_strategy: doctrine.orm.naming_strategy.underscore
                mappings:
                    Site:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/ConfigurationConnexion'
                        prefix: 'App\ConfigurationConnexion'
                        alias: Configuration

Maintenant votre API peut switcher de base de données suivant l’host pour enregistrer les données.

Partager cet article

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.