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!

3
  • By "dynamic database connection," do you mean creating a DBAL connection from your controller? Commented Jun 20, 2011 at 18:51
  • 3
    yes, exactly! Be able to change from one database to another, and these databases may not be declared in the config.yml file Commented Jun 21, 2011 at 7:01
  • getEntityManager is deprecated in latest version of symfony2 Commented Feb 26, 2014 at 17:15

3 Answers 3

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 [[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;
    }
}
}

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
Sign up to request clarification or add additional context in comments.

8 Comments

What's the purpose of dynamic_conn or dynamic_manager? Can you elaborate more on what their purpose is? Thanks
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
Thanks for this response. With the help of knpuniversity.com/screencast/question-answer-day/… I was able to select databases based on the given subdomain
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...
You have to do this by CompilerPass. Directly (richardmiller.co.uk/2012/02/15/…) or by injecting whole container (like I did with session service), into your wrapper. ID: service_container.
|
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.

4 Comments

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%
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
It seems that in Symfony 2.1 you cannot access the container property directly, you need to use the helper method $this->getContainer()->get('...')
Awesome short example on how to get a multi-db factory going, +1 to you sir.
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);
}

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.