【问题标题】:Symfony2, Dynamic DB Connection/Early override of Doctrine ServiceSymfony2,动态数据库连接/早期覆盖 Doctrine 服务
【发布时间】:2013-02-27 09:40:39
【问题描述】:

我有一个核心配置数据库,每一行都是一个带有一些基本配置等的“应用程序”。
一旦您选择了您的应用,我想使用该行 (ID) 的属性连接到数据库,并且主机也可能会根据该行而更改。

我想要的是注册一个使用这些详细信息设置 Doctrine 服务的服务,如果你在网站上需要它的地方(我知道基于 URI)。

我正在使用实体管理器,以及各种 Doctrine Listeners/Event subs

我使用过 ConnectionFactory,但这似乎会导致订阅者出现问题。

连接可以透明地修改 Doctrine 服务的最佳方法是什么,以便控制器可以在不知道它们连接到哪个数据库主机和数据库名称的情况下进行操作?

这种类型的每个数据库都将具有相同的结构,因此所有实体映射都是正确的。

我正在寻找一个非常干净的实现,希望使用服务容器来避免任何“黑客攻击”。

有人知道这方面的知识吗?

【问题讨论】:

    标签: php symfony doctrine-orm


    【解决方案1】:

    结合起来,这两个帖子帮助我解决了我自己非常相似的问题。这是我的解决方案,也许对其他人有用:

    <?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
                    }
                }
            }
        }
    }
    

    为了让它工作,我设置 services.yml 如下:

    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 }
    

    我有这个路由配置来获取 _site 参数,在我的例子中,它是 URL 的一部分,但您可能可以根据您的设置以其他方式获取它:

    resource: "@CCollabBundle/Controller"
    type:     annotation
    prefix:   /{_site}
    defaults:
     _site: default
    

    【讨论】:

    • 这是正确的,因为在我的示例中,我使用子域来确定要访问哪个数据库。必须明确地为控制台命令提供此信息并进行自己的切换。
    【解决方案2】:

    这是新的和改进的非反射版本

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

    【讨论】:

    • Doctrine 也整理了一下,并且默认连接是注入的,所以可以去掉ContainerAware扩展。
    • $this->request->属性从何而来?
    【解决方案3】:

    在 symfony 4 中,您可以使用包装类来实现:

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

    该类只是扩展了原来的 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
            );
    
        }
    }
    

    【讨论】:

    • changeDatabaseConnection 中不存在所以没有效果!
    • @dan-zauner,那怎么用呢?
    • 我猜建议是在您的对象上运行时调用changeDatabase(这意味着它必须使用 DynamicConnection 类输入)。 IMO 看起来不太好
    【解决方案4】:

    Symfony 4

    在服务定义中使用装饰器模式的最简洁方式:

    首先创建一个自定义类,如 App\Factory\Authentication\DatabaseConnectionFactory 然后这个类将实例化一个教义.dbal.connection_factory的实例。

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

    在我们的自定义连接工厂类中模拟 createConnection() 函数并通过调用 WrappedConnectionFactory 上的函数来执行原始的 createConnection() 逻辑 (= 学说.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);
    }
    

    【讨论】:

      【解决方案5】:

      我查看了您的服务并尝试实现它,但您似乎缺少一些需要传递给构造函数的参数。这是一个应该可以工作的更新版本:

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

      【讨论】:

      • 嘿,已经更新了我在推特上提到的答案。新的和改进的,不使用反射。
      猜你喜欢
      • 2014-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多