Symfony:动态更改数据库

问题描述:

假设我有3个数据库:

let's say I have 3 databases:

  • prefix_db1
  • prefix_db2
  • prefix_db3
  • prefix_db1
  • prefix_db2
  • prefix_db3

而且我想像这样的http://localhost/my-project/web/app_dev.php/db1/books从url动态连接到它们,所以我知道要从url连接到哪个数据库(在本例中为prefix_db1)
基本上,这个想法是准备一个将随每个 http请求触发的监听器,从url获取数据库名称,然后覆盖doctrin的参数,如下所示:
services.yml 中:

And I want to connect to them dynamically from the url like this http://localhost/my-project/web/app_dev.php/db1/books so I know which database to conenct to from the url (in this case prefix_db1)
And basically the idea was to prepare a listener that will be fired with each http request, get the database name from the url and then override doctrin's params, something like this:
Within services.yml:

dynamic_connection:
        class: AppBundle\service\DynamicDBConnector
        arguments:  ['@request_stack']
        calls:
            - [ setDoctrineConnection, ['@doctrine.dbal.default_connection'] ]
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

我的听众:

<?php    
namespace AppBundle\service;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\RequestStack;
use Exception;

class DynamicDBConnector
{
    /**
     * @var Connection
     */
    private $connection;

    /*
     * @var Request
     */
    private $request;


    public function __construct(RequestStack $requestStack)
    {
        $this->request = $requestStack->getCurrentRequest();

    }

    /**
     * Sets the DB Name prefix to use when selecting the database to connect to
     *
     * @param  Connection       $connection
     * @return DynamicDBConnector $this
     */
    public function setDoctrineConnection(Connection $connection)
    {
        $this->connection = $connection;
        return $this;
    }

    public function onKernelRequest()
    {
        if ($this->request->attributes->has('_company')) {

            $connection = $this->connection;
            $params     = $this->connection->getParams();

            $companyName = $this->request->get('_company');
            // I did the concatenation here because in paramaters.yml I just put the prefix (database_name: prefix_) so after the concatenation I get the whole database name "prefix_db1"
            $params['dbname'] = $params['dbname'] . $companyName;

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

现在这很好用了,我已经用一个简单的书籍列表对其进行了测试,每次更改url时,我都会得到与每个数据库相关的列表:

Now this worked very well I have tested it using a simple list of books and each time I change the url I get the list related to each database:

http://localhost/my-project/web/app_dev.php/db1/books // I get books of database prefix_db1

http://localhost/my-project/web/app_dev.php/db2/books // I get books of database prefix_db2

现在让我们来解决问题吧:):
现在的问题是,当我使用身份验证系统保护我的项目并尝试使用此URL http://localhost/my-project/web/app_dev.php/db1/login登录(当然每个数据库都有user表)时 我收到此异常:

Now let's get to the problem shall we :):
The problem now is that when I secure my project with authentication system and try to login (of course each database has user table) using this url http://localhost/my-project/web/app_dev.php/db1/login I get this exception :

An exception occured in driver: SQLSTATE[HY000] [1049] Base 'prefix_' unknown

您可以看到symfony尝试使用 parameters.yml 中声明的database_name登录用户,这意味着symfony的security_checker已在我的听众之前以及在覆盖教义的params.

我的问题:
有没有办法在其他任何HTTP请求侦听器之前触发我的侦听器?或者可能是另一种解决方案,以确保对数据库的任何请求都必须使用正确的数据库名称.
抱歉,很长的帖子.


来自symfony的官方文档:
https://symfony.com/doc/2.3/cookbook/event_dispatcher/event_listener.html

As you can see symfony tried to login the user using the database_name declared in parameters.yml which means that the security_checker of symfony has been fired before my listener and before overriding Doctrine's params.

My question:
Is there any way to fire my listener before any other http request listener ? or maybe an alternative solution to make sure that any request to database must be with the right database name.
Sorry for the long post.


From the official documentation of symfony:
https://symfony.com/doc/2.3/cookbook/event_dispatcher/event_listener.html

另一个可选的标签属性称为优先级,默认情况下 设置为0,并控制侦听器的执行顺序( 优先级最高,则监听器执行得越早).这是 当您需要确保在执行之前先执行一个侦听器时很有用 其他.内部Symfony侦听器的优先级通常 范围从-255到255,但您自己的听众可以使用任何正数或 负整数.

The other optional tag attribute is called priority, which defaults to 0 and it controls the order in which listeners are executed (the highest the priority, the earlier a listener is executed). This is useful when you need to guarantee that one listener is executed before another. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer.

我将侦听器的优先级设置为 10000 :

I set the priority of my listener to 10000:

tags:
    - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10000 }

但是问题仍然存在,仍然无法在symfony之前解雇我的听众!

But the problem persist, still can't fire my listener before symfony!

很好的解决方案,但是如果您想从URL获取参数_company,则可以通过传入参数的EventManager对象在构造函数中检索容器,从中获取当前请求,实际上是将容器注入到ContainerAwareEventManager的子类EventManager

Great solution but if you want get the parameter _company from the URL you can retrieve the container inside the constructor through the EventManager object passed in parameters and get the current request from it, in fact the container is injected into ContainerAwareEventManager the sub class of EventManager

class DynamicDBConnector extends Connection
{
    public function __construct($params, $driver, $config, $eventManager)
    {
        if(!$this->isConnected()){
            // Create default config and event manager if none given (case in command line)
            if (!$config) {
                $config = new Configuration();
            }
            if (!$eventManager) {
                $eventManager = new EventManager();
            }

            $refEventManager = new \ReflectionObject($eventManager);
            $refContainer = $refEventManager->getProperty('container');
            $refContainer->setAccessible('public'); //We have to change it for a moment

            /*
             * @var \Symfony\Component\DependencyInjection\ContainerInterface $container
             */
            $conrainer = $refContainer->getValue($eventManager);

            /*
             * @var Symfony\Component\HttpFoundation\Request
             */
            $request = $conrainer->get('request_stack')->getCurrentRequest();

            if ($request != null && $request->attributes->has('_company')) {
                $params['dbname'] .= $request->attributes->get('_company');
            }

            $refContainer->setAccessible('private'); //We put in private again
            parent::__construct($params, $driver, $config, $eventManager);
        }
    }
}