我有一个Core配置数据库,每行都是一个'App',带有一些基本配置等。
一旦选择了应用程序,我想使用该行的属性(ID)连接到数据库,主机也可能会根据该行进行更改。
我想要的是注册一个使用这些详细信息设置Doctrine服务的服务,如果您在网站上的所需位置(我知道基于URI)。
我正在使用实体管理器和各种Doctrine Listeners / Event subs
我玩过ConnectionFactory,但这似乎会导致订阅者出现问题。
最好的方法是什么是透明地修改Doctrine服务,以便控制器可以在不知道他们连接到哪个DB主机和DB名称的情况下执行操作?
此类型的每个DB都具有相同的结构,因此所有实体映射都是正确的。
我正在寻找一个非常干净的实现,希望使用服务容器来避免任何“黑客攻击”。
有没有人知道这样做?
答案 0 :(得分:11)
结合起来,这两个帖子帮助我解决了我自己非常相似的问题。这是我的解决方案,也许对其他人有用:
<?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
答案 1 :(得分:10)
这是新的和改进的非反射版本
#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;
}
}
答案 2 :(得分:3)
在symfony 4中,您可以使用包装类将其拉出来:
# doctrine.yaml
doctrine:
dbal:
connections:
default:
wrapper_class: App\Service\Database\DynamicConnection
该类只是扩展了原始的连接:
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
);
}
}
答案 3 :(得分:0)
我看了一下你的服务并尝试实现它,但看起来你错过了一些需要传递给你的构造函数的参数。这是一个应该有效的更新版本:
#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');
}
}
答案 4 :(得分:0)
Symfony 4
在服务定义中使用装饰器模式的最简洁方法:
首先创建一个自定义类,例如App \ Factory \ Authentication \ DatabaseConnectionFactory 然后,此类将使用doctrine.dbal.connection_factory的实例进行实例化。
#services.xml
App\Factory\Authentication\DatabaseConnectionFactory:
decorates: doctrine.dbal.connection_factory
arguments:
$wrappedConnectionFactory: '@App\Factory\Authentication\DatabaseConnectionFactory.inner'
在我们的自定义连接工厂类中,对createConnection()函数进行模拟,并通过调用wrappedConnectionFactory(= doctrine.dbal.connection_factory)上的函数来执行原始的createConnection()逻辑。
/** 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);
}