假设我有 3 个数据库:
我想从这样的 url 动态连接到它们
http://localhost/my-project/web/app_dev.php/db1/books
所以我知道要从 url 连接到哪个数据库(在本例中 prefix_db1
)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 时,我都会得到与每个数据库相关的列表:
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
现在让我们解决问题吧:):
现在的问题是,当我使用身份验证系统保护我的项目并尝试使用此网址登录(当然每个数据库都有
user
表)时http://localhost/my-project/web/app_dev.php/db1/login
我得到这个例外:An exception occured in driver: SQLSTATE[HY000] [1049] Base 'prefix_' unknown
正如你所看到的,symfony 尝试使用
parameters.yml中声明的
database_name
来登录用户,这意味着 symfony 的 security_checker
已经在我的监听器之前以及覆盖 Doctrine 的 params
之前被触发。另一个可选标签属性称为优先级,默认为 为 0,它控制监听器的执行顺序( 优先级最高,侦听器执行得越早)。这是 当您需要保证之前执行一个侦听器时很有用 其他。内部 Symfony 监听器的优先级通常 范围从 -255 到 255,但您自己的听众可以使用任何正数或 负整数。
我将侦听器的优先级设置为10000:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10000 }
但是问题依然存在,仍然无法在 symfony 之前解雇我的听众!
我找到了解决办法
这个想法是更改 symfony 用于创建数据库连接的默认
Connection
类:doctrine:
dbal:
connections:
default:
wrapper_class: AppBundle\Doctrine\DynamicConnection
driver: pdo_mysql
host: '%database_host%'
port: '%database_port%'
dbname: '%database_name%'
user: '%database_user%'
password: '%database_password%'
charset: UTF8
之后我们可以更改构造函数中给定的参数:
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
class DynamicConnection extends Connection
{
public function __construct(array $params, Driver $driver, $config, $eventManager)
{
$params['dbname'] = 'teqsdqsdqst';
parent::__construct($params, $driver, $config, $eventManager);
}
}
现在我们只需要从url中获取参数并在里面设置即可
$params['dbname']
。很好的解决方案,但如果你想从 URL 获取参数
_company
,你可以通过传入参数的 EventManager
对象检索构造函数内的容器,并从中获取当前请求,实际上容器被注入到 ContainerAwareEventManager
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);
}
}
}
另一种方法是使用 TvC 在 Symfony2,动态数据库连接/早期覆盖 Doctrine Service 中提出的doctrine.dbal.connection_factory。 我在 Symfony 6 中做到了:
#services.yaml
App\Routing\DBAL\DatabaseConnectionFactory:
decorates: doctrine.dbal.connection_factory
arguments:
$requestStack: '@request_stack'
#DatabaseConnectionFactory.php
namespace App\Routing\DBAL;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
class DatabaseConnectionFactory extends ConnectionFactory
{
private $requestStack;
public function __construct( $requestStack )
{
$this->requestStack = $requestStack;
}
public function createConnection(
array $params,
Configuration $config = null,
EventManager $eventManager = null,
array $mappingTypes = []
)
{
$uri = null;
$request = $this->requestStack->getCurrentRequest();
if( $request ) {
$uri = $request->getUri();
}
if( $uri && str_contains($uri, 'c/lmh/pathology') ) {
//here logic to get dbname
$dbName = 'Tenant2';
$params['dbname'] = $dbName;
} else {
//don't change default dbname
}
return parent::createConnection($params, $config, $eventManager, $mappingTypes);
}
}
您应该在 config.yml 中添加数据库名称,如下所示:
orm:
auto_generate_proxy_classes: '%kernel.debug%'
# naming_strategy: doctrine.orm.naming_strategy.underscore
# auto_mapping: true
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
DataMiningBundle: ~
AppBundle: ~
UserBundle: ~
your_second_db:
connection: your_second_db (decalared in parameters.yml)
mappings:
yourBundle: ~
并从您的控制器调用它:
$em = $doctrine->getConnection('your_second_db');