53

I am quite new to SF2 and I was wondering how I could manage connections to severals databases into ONE bundle. For the moment I have this solution - which works fine - but I don't know if it is the right way to do it....

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

And then, in order to switch to one of the BD or the other, I do :

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

So guys, do you think it is a good way to manage this?

And my second question is :

how to set up dynamic database connection? I mean I have 100 databases in my system and I can't set all them in my config.yml file. So I would like to be able to change database on the fly.

Thanks for the help!

Fish
  • 671
  • 1
  • 6
  • 6

3 Answers3

53

If you use ConnectionFactory, your event subscribers attached to the connection will stop working, for example stofDoctrineExtensions.

Here is my method. I have as with ConnectionFactory have empty connection and EntityManager. While working I just replace connection configuration by Reflections. Works on 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)
  }
}

UPDATE:

More elegant solution for doctrine 2.2 / sf 2.3 (without relection), created for php5.4 (I love new array initializer :D) We can use doctrine feature called connection wrapper, see http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/portability.html

This example use session service for temporary storing connection details.

At first we have to create special connection wrapper:

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 [zulus@w3des.net]
 */
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;
    }
}
}

Next register it in your doctrine configuration:

…

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

And our ConnectionWrapper is properly registered. Now session injection.

First create special CompilerPass class:

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')
    ]);
}
}

And we record our new compiler class in *Bundle class:

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

And that its all!

Connection will be created on demand, based on session properties.

To switch database, just use:

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

Advantages

  1. No more reflection
  2. Creation on demand
  3. Elegant and powerfull

Disadvantages

  1. You have to manualy cleanup your entity manager, or create special doctrine event for this
  2. Much more code
zulus
  • 2,506
  • 19
  • 20
  • 1
    What's the purpose of dynamic_conn or dynamic_manager? Can you elaborate more on what their purpose is? Thanks – Reza S Aug 16 '12 at 17:11
  • I have application, where each client have own database with the same schema like other clients. This method allows me switch to real database by for ex. session privileges – zulus Mar 14 '13 at 20:24
  • Thanks for this response. With the help of http://knpuniversity.com/screencast/question-answer-day/symfony2-dynamic-subdomains I was able to select databases based on the given subdomain – Rick Slinkman Oct 29 '13 at 08:50
  • Ok, but what about ParamConverter? Can I use it in annotation aswell? `/** * @Route("/page/{id}.{_format}", defaults={"_format"="json"}) * @ParamConverter("page", class="BlogBundle:Rest\Page", options={"entity_manager" = "dynamic"}) */ public function showAction(\BlogBundle\Entity\Rest\Page $page) { //... }` Can I get that working too? The correct entity manager should be injected into ParamConverter by annotation... – thorinkor Feb 04 '14 at 08:24
  • @zulus How can I get values from container in "connection wrapper" class? Tried to do the same how session is set up, but had no success... – Satevg Mar 11 '15 at 19:11
  • 1
    You have to do this by CompilerPass. Directly (http://richardmiller.co.uk/2012/02/15/symfony2-service-container-compiler-passes/) or by injecting whole container (like I did with session service), into your wrapper. ID: service_container. – zulus Mar 12 '15 at 10:13
  • @zulus Much thanks!! Instead of 'service_container' id I used by guess 'container'. Now it works, great solution at all! – Satevg Mar 12 '15 at 10:23
  • looks like you should switch 2 lines of code - params is always undefined in first one $driverOptions = isset($params['driverOptions']) ?$params['driverOptions'] : array(); $params = $this->getParams(); – Andrew Zhilin May 14 '17 at 17:05
23

You can look into Symfony\Bundle\DoctrineBundle\ConnectionFactory, using the container service 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',
));

That's just a quick example, but it should get you started.

Problematic
  • 17,567
  • 10
  • 73
  • 85
  • Thank you, it helped me! I was wondering if there is a way to change the value of a parameter which is defined in one of my config file? For example : # app/config/config.yml parameters: my_mailer.class: Acme\HelloBundle\Mailer my_mailer.transport: sendmail services: my_mailer: class: %my_mailer.class% arguments: [%my_mailer.transport%] Can I change the value of : %my_mailer.transport% – Fish Jun 24 '11 at 14:55
  • 2
    I managed to create a connection "on the fly" by defining an entity which is associated to a fake dbal.connection item - in my config.yml file - and then I use the doctrine.dbal.connection_factory to create the right connection and finally I set this new connection to the $this->container `$this->container->set('doctrine.dbal.custom_client_connection', $connection); $myObject = $this->get('doctrine') ->getEntityManager('custom_client') ->getRepository('FooBarBundle:MyObject) ->find($id);` I don't know if it is the right way / best way, but it works – Fish Jul 04 '11 at 15:05
  • It seems that in Symfony 2.1 you cannot access the container property directly, you need to use the helper method `$this->getContainer()->get('...')` – David Barreto Nov 08 '12 at 21:58
  • Awesome short example on how to get a multi-db factory going, +1 to you sir. – ehime Jul 02 '13 at 16:42
0

I run into the same needing to have different databases with the same schema for each client. Since symfony 2.3, after the deprecation of the method resetEntityManager, i noticed that the code run well without closing the connection and without resetting the (old Entity) Manager.

this is my current working code:

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);
}
bal3no
  • 1
  • 1
  • I am building a dashboard system that will integrate different databases into one thereby providing an interface to manage those databases with Symfony3.0. The thing is doctrine uses the parameters.yml file to declare connections and I have not seen anyone talking about how to achieve the same thing as contained in this thread in that perspective. The docs are not making it clear either as I have tried them I keep on stumbling to errors. Can somebody provide a solution for Symfony3.0 please.... Thanks. – scruffycoder86 Mar 04 '16 at 11:14