可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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 row (ID), and the host may also change based on the row.
What I want is to register a service that sets up the Doctrine service using these details if you are in a place on the site that it's required (which I know based on URI).
I am using the Entity manager, and various Doctrine Listeners/Event subs
I've played around with the ConnectionFactory, but this appears to cause problems with the subscribers.
What is the best way to hook something up that will transparently modify the Doctrine service, so that the controllers can act without any knowledge of which DB host and DB name they are connecting to?
Each DB of this type will have the same structure so all Entity mapping is correct.
I'm looking for a really clean implementation, hopefully using the Service Container to avoid any 'hacks'.
Does anyone have any knowledge of doing this?
回答1:
Combined, these two postings helped me solve my own very similar problem. Here is my solution, maybe it is useful for someone else:
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
回答2:
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]] 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; } }
回答3:
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("/(?getDatabase())), $dbName, $params['url'] ); } if (isset($params['dbname'])) $params['dbname'] = $dbName; parent::__construct( $params, $this->_driver, $this->_config, $this->_eventManager ); } }
回答4:
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 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'); } }