web-dev-qa-db-fra.com

Symfony 2: connexion à une base de données multiple et dynamique

Je suis assez nouveau sur SF2 et je me demandais comment gérer les connexions aux différentes bases de données en un seul paquet. Pour le moment, j'ai cette solution - qui fonctionne bien - mais je ne sais pas si c'est la bonne façon de le faire ...

dans myBundle\Ressource\config\config.yml:

doctrine:
dbal:
    default_connection:       default
    connections:
        default:
            dbname:           SERVER
            user:             root
            password:         null
            Host:             localhost
        client:
            dbname:           CLIENT_134
            user:             root
            password:         null
            Host:             localhost
orm:
    default_entity_manager:   default
    entity_managers:
        default:
            connection:       default
            mappings:
                MyBundle: ~
        client:
            connection:       client
            mappings:
                MyBundle: ~

Et puis, pour passer à l'un des BD ou à l'autre, je fais:

$O_ressource=  $this->get('doctrine')->getEntityManager('client');
$O_ressource=  $this->get('doctrine')->getEntityManager('default');

Alors, les gars, pensez-vous que c'est une bonne façon de gérer cela?

Et ma deuxième question est:

comment configurer une connexion de base de données dynamique? Je veux dire que j'ai 100 bases de données dans mon système et je ne peux pas les définir toutes dans mon fichier config.yml. Je voudrais donc pouvoir changer de base de données à la volée.

Merci pour l'aide!

53
Fish

Si vous utilisez ConnectionFactory, vos abonnés à l'événement connectés à la connexion cesseront de fonctionner, par exemple stofDoctrineExtensions.

Voici ma méthode. J'ai comme avec ConnectionFactory j'ai une connexion vide et EntityManager. Tout en travaillant, je remplace simplement la configuration de la connexion par Reflections. Fonctionne sur SF 2.0.10;)

class YourService extends ContainerAware
{ 

  public function switchDatabase($dbName, $dbUser, $dbPass) 
  {
    $connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));
    $connection->close();

    $refConn = new \ReflectionObject($connection);
    $refParams = $refConn->getProperty('_params');
    $refParams->setAccessible('public'); //we have to change it for a moment

    $params = $refParams->getValue($connection);
    $params['dbname'] = $dbName;
    $params['user'] = $dbUser;
    $params['password'] = $dbPass;

    $refParams->setAccessible('private');
    $refParams->setValue($connection, $params);
    $this->container->get('doctrine')->resetEntityManager('dynamic_manager'); // for sure (unless you like broken transactions)
  }
}

[~ # ~] mise à jour [~ # ~] :

Solution plus élégante pour doctrine 2.2/sf 2.3 (sans relection), créée pour php5.4 (J'adore le nouveau tableau initialiseur: D) Nous pouvons utiliser doctrine fonctionnalité appelée wrapper de connexion, voir http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/portability.html

Cet exemple utilise le service de session pour stocker temporairement les détails de la connexion.

Au début, nous devons créer un wrapper de connexion spécial:

namespace w3des\DoctrineBundle\Connection;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Session\Session;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Event\ConnectionEventArgs;

/*
 * @author Dawid zulus Pakula [[email protected]]
 */
class ConnectionWrapper extends Connection
{

const SESSION_ACTIVE_DYNAMIC_CONN = 'active_dynamic_conn';

/**
 * @var Session
 */
private $session;

/**
 * @var bool
 */
private $_isConnected = false;

/**
 * @param Session $sess
 */
public function setSession(Session $sess)
{
    $this->session = $sess;
}

public function forceSwitch($dbName, $dbUser, $dbPassword)
{
    if ($this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
        $current = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
        if ($current[0] === $dbName) {
            return;
        }
    }

    $this->session->set(self::SESSION_ACTIVE_DYNAMIC_CONN, [
        $dbName,
        $dbUser,
        $dbPass
    ]);

    if ($this->isConnected()) {
        $this->close();
    }
}

/**
 * {@inheritDoc}
 */
public function connect()
{
    if (! $this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
        throw new \InvalidArgumentException('You have to inject into valid context first');
    }
    if ($this->isConnected()) {
        return true;
    }

    $driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array();

    $params = $this->getParams();
    $realParams = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
    $params['dbname'] = $realParams[0];
    $params['user'] = $realParams[1];
    $params['password'] = $realParams[2];

    $this->_conn = $this->_driver->connect($params, $params['user'], $params['password'], $driverOptions);

    if ($this->_eventManager->hasListeners(Events::postConnect)) {
        $eventArgs = new ConnectionEventArgs($this);
        $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
    }

    $this->_isConnected = true;

    return true;
}

/**
 * {@inheritDoc}
 */
public function isConnected()
{
    return $this->_isConnected;
}

/**
 * {@inheritDoc}
 */
public function close()
{
    if ($this->isConnected()) {
        parent::close();
        $this->_isConnected = false;
    }
}
}

Enregistrez-le ensuite dans votre configuration doctrine:

…

connections:
  dynamic:
    driver:   %database_driver%
    Host:     %database_Host%
    port:     %database_port%
    dbname:   'empty_database'
    charset:  UTF8
    wrapper_class: 'w3des\DoctrineBundle\Connection\ConnectionWrapper'

Et notre ConnectionWrapper est correctement enregistré. Maintenant injection de session.

Créez d'abord une classe CompilerPass spéciale:

namespace w3des\DoctrineBundle\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class ConnectionCompilerPass implements CompilerPassInterface
{

/**
 * {@inheritDoc}
 */
public function process(ContainerBuilder $container)
{
    $connection = $container
    ->getDefinition('doctrine.dbal.dynamic_connection')
    ->addMethodCall('setSession', [
        new Reference('session')
    ]);
}
}

Et nous enregistrons notre nouvelle classe de compilateur dans la classe * Bundle:

public function build(ContainerBuilder $container)
{
    parent::build($container);
    $container->addCompilerPass(new ConnectionCompilerPass());
}

Et c'est tout!

La connexion sera créée à la demande, en fonction des propriétés de la session.

Pour changer de base de données, utilisez simplement:

$this->get('doctrine.dbal.dynamic_connection')->forceSwitch($dbname, $dbuser, $dbpass);

Avantages

  1. Plus de réflexion
  2. Création à la demande
  3. Élégant et puissant

Inconvénients

  1. Vous devez nettoyer manuellement votre gestionnaire d'entités ou créer un événement spécial doctrine pour cela
  2. Beaucoup plus de code
52
zulus

Vous pouvez consulter Symfony\Bundle\DoctrineBundle\ConnectionFactory, en utilisant le service de conteneur doctrine.dbal.connection_factory:

$connectionFactory = $this->container->get('doctrine.dbal.connection_factory');
$connection = $connectionFactory->createConnection(array(
    'driver' => 'pdo_mysql',
    'user' => 'root',
    'password' => '',
    'Host' => 'localhost',
    'dbname' => 'foo_database',
));

Ce n'est qu'un exemple rapide, mais cela devrait vous aider à démarrer.

23
Problematic

Je rencontre le même besoin d'avoir différentes bases de données avec le même schéma pour chaque client. Depuis symfony 2.3, après la dépréciation de la méthode resetEntityManager, j'ai remarqué que le code fonctionnait bien sans fermer la connexion et sans réinitialiser le (ancienne entité) Manager.

c'est mon code de travail actuel:

public function switchDatabase($dbName, $dbUser, $dbPass) {
    $connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));

    $refConn = new \ReflectionObject($connection);
    $refParams = $refConn->getProperty('_params');
    $refParams->setAccessible('public'); //we have to change it for a moment

    $params = $refParams->getValue($connection);
    $params['dbname'] = $dbName;
    $params['user'] = $dbUser;
    $params['password'] = $dbPass;

    $refParams->setAccessible('private');
    $refParams->setValue($connection, $params);
}
0
bal3no