Symfony2, Dynamic DB Connection/Early override of Doctrine Service

前端 未结 5 2025
别那么骄傲
别那么骄傲 2020-11-29 04:24

I have a Core config Database, each row is an \'App\' with some basic config etc.
Once you have chosen your app, I want to connect to a database using a property of that

相关标签:
5条回答
  • 2020-11-29 04:41

    In symfony 4, you can pull it off with a wrapper class:

    # doctrine.yaml
    doctrine:
        dbal:
          connections:
            default:
              wrapper_class: App\Service\Database\DynamicConnection
    

    The class simply extends the original Connection:

    class DynamicConnection extends \Doctrine\DBAL\Connection
    {
    
        public function changeDatabase(string $dbName)
        {
            $params = $this->getParams();
    
            if ($this->isConnected())
                $this->close();
    
            if (isset($params['url'])) {
                $params['url'] = preg_replace(
                    sprintf("/(?<=\/)%s/", preg_quote($this->getDatabase())),
                    $dbName,
                    $params['url']
                );
            }
    
            if (isset($params['dbname']))
                $params['dbname'] = $dbName;
    
            parent::__construct(
                $params,
                $this->_driver,
                $this->_config,
                $this->_eventManager
            );
    
        }
    }
    
    0 讨论(0)
  • 2020-11-29 04:42

    Here is the new and improved non-reflection version

    #services.yml
    acme_app.dynamic_connection:
        class: %acme.dynamic_doctrine_connection.class%
        calls:
            - [setDoctrineConnection, [@doctrine.dbal.default_connection]]
    
    
    <?php
    
    namespace Acme\Bundle\AppBundle;
    
    use Doctrine\DBAL\Connection;
    use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
    use Exception;
    
    class DynamicDoctrineConnection
    {
        /**
         * @var Connection
         */
        private $connection;
    
        /**
         * Sets the DB Name prefix to use when selecting the database to connect to
         *
         * @param  Connection       $connection
         * @return SiteDbConnection $this
         */
        public function setDoctrineConnection(Connection $connection)
        {
            $this->connection = $connection;
    
            return $this;
        }
    
        public function setUpAppConnection()
        {
            if ($this->request->attributes->has('appId')) {
                $connection = $this->connection;
                $params     = $this->connection->getParams();
    
                // we also check if the current connection needs to be closed based on various things
                // have left that part in for information here
                // $appId changed from that in the connection?
                // if ($connection->isConnected()) {
                //     $connection->close();
                // }
    
                // Set default DB connection using appId
                //$params['host']   = $someHost;
                $params['dbname'] = 'Acme_App'.$this->request->attributes->get('appId');
    
                // Set up the parameters for the parent
                $connection->__construct(
                    $params, $connection->getDriver(), $connection->getConfiguration(),
                    $connection->getEventManager()
                );
    
                try {
                    $connection->connect();
                } catch (Exception $e) {
                    // log and handle exception
                }
            }
    
            return $this;
        }
    }
    
    0 讨论(0)
  • 2020-11-29 04:51

    I took a look at your service and tried to implement it, but it looks like you were missing some arguments that needed passed into your constructor. Here is an updated version that should work:

    #services.yml
    parameters:
        acme_page.dynamic_doctrine_connection.class: Acme\Bundle\PageBundle\DynamicDoctrineConnection
    
    services:
        acme_page.dynamic_doctrine_connection:
            class:      %acme_page.dynamic_doctrine_connection.class%
            arguments:  [@request, @doctrine.dbal.client_connection, @doctrine]
            scope:      request
            calls:
                - [setContainer, [@service_container]]
            tags:
                - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
    
    //DynamicDoctrineConnection.php
    <?php
    
    namespace Acme\Bundle\PageBundle;
    
    use Symfony\Component\HttpFoundation\Request;
    use Doctrine\DBAL\Connection;
    use Doctrine\Bundle\DoctrineBundle\Registry;
    
    /**
     * Creates a Doctrine connection from attributes in the Request
     */
    class DynamicDoctrineConnection
    {
        private $request;
        private $defaultConnection;
        private $doctrine;
    
        public function __construct(Request $request, Connection $defaultConnection, Registry $doctrine)
        {
            $this->request           = $request;
            $this->defaultConnection = $defaultConnection;
            $this->doctrine          = $doctrine;
        }
    
        public function onKernelRequest()
        {
            if ($this->request->attributes->has('appId')) {
    
                $dbName             = 'Acme_App_'.$this->request->attributes->get('appId');
    
                $this->defaultConnection->close();
    
                $reflectionConn     = new \ReflectionObject($this->defaultConnection);
                $reflectionParams   = $reflectionConn->getProperty('_params');
                $reflectionParams->setAccessible(true);
    
                $params             = $reflectionParams->getValue($this->defaultConnection);
                $params['dbname']   = $dbName;
    
                $reflectionParams->setValue($this->defaultConnection, $params);
                $reflectionParams->setAccessible(false);
    
                $this->doctrine->resetEntityManager('default');
        }
    }
    
    0 讨论(0)
  • 2020-11-29 04:52

    Combined, these two postings helped me solve my own very similar problem. Here is my solution, maybe it is useful for someone else:

    <?php
    
    namespace Calitarus\CollaborationBundle\EventListener;
    
    use Symfony\Component\HttpFoundation\Request;
    use Doctrine\DBAL\Connection;
    use Exception;
    use Monolog\Logger;
    
    
    
    class DatabaseSwitcherEventListener {
    
        private $request;
        private $connection;
        private $logger;
    
        public function __construct(Request $request, Connection $connection, Logger $logger) {
            $this->request = $request;
            $this->connection = $connection;
            $this->logger = $logger;
        }
    
    
        public function onKernelRequest() {
            if ($this->request->attributes->has('_site')) {
                $site = $this->request->attributes->get('_site');
    
                $connection = $this->connection;
                $params     = $this->connection->getParams();
    
                $db_name = 'br_'.$this->request->attributes->get('_site');
                // TODO: validate that this site exists
                if ($db_name != $params['dbname']) {
                    $this->logger->debug('switching connection from '.$params['dbname'].' to '.$db_name);
                    $params['dbname'] = $db_name;
                    if ($connection->isConnected()) {
                        $connection->close();
                    }
                    $connection->__construct(
                        $params, $connection->getDriver(), $connection->getConfiguration(),
                        $connection->getEventManager()
                    );
    
                    try {
                        $connection->connect();
                    } catch (Exception $e) {
                        // log and handle exception
                    }
                }
            }
        }
    }
    

    To get this to work, I set up services.yml as follows:

    services:
        cc.database_switcher:
            class:      Calitarus\CollaborationBundle\EventListener\DatabaseSwitcherEventListener
            arguments:  [@request, @doctrine.dbal.default_connection, @logger]
            scope:      request
            tags:
                - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
    

    and I have this routing configuration to get the _site parameter, which in my case is part of the URL, but you can probably get it in other ways depending on your setup:

    resource: "@CCollabBundle/Controller"
    type:     annotation
    prefix:   /{_site}
    defaults:
     _site: default
    
    0 讨论(0)
  • 2020-11-29 05:00

    Symfony 4

    The cleanest way by using a decorator pattern in the service definition:

    First create a custom class like App\Factory\Authentication\DatabaseConnectionFactory Then this class will instantiate with an instance of doctrine.dbal.connection_factory.

    #services.xml
    App\Factory\Authentication\DatabaseConnectionFactory:
            decorates: doctrine.dbal.connection_factory
            arguments:
                $wrappedConnectionFactory: '@App\Factory\Authentication\DatabaseConnectionFactory.inner'
    

    In our custom connection factory class mock the createConnection() function and execute the original createConnection() logic by calling the function on the wrappedConnectionFactory (= doctrine.dbal.connection_factory).

    /** App\Factory\Authentication\DatabaseConnectionFactory
     * @param array              $params
     * @param Configuration|null $config
     * @param EventManager|null  $eventManager
     * @param array              $mappingTypes
     *
     * @throws \DomainException
     *
     * @return mixed
     */
    public function createConnection(array $params, Configuration $config = null, EventManager $eventManager = null, array $mappingTypes = [])
    {
        $params['url'] = $this->databaseConnectionUrlService->getDatabaseConnectionUrlForApiUser($this->apiUser, $params['url'] );
    
        return $this->wrappedConnectionFactory->createConnection($params, $config, $eventManager, $mappingTypes);
    }
    
    0 讨论(0)
提交回复
热议问题