Skip to content

Commit 21b4dd7

Browse files
authored
use configurable keys for keeping static connections (#296)
1 parent e372456 commit 21b4dd7

File tree

15 files changed

+287
-120
lines changed

15 files changed

+287
-120
lines changed

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,51 @@ dama_doctrine_test:
108108
connection_a: true
109109
```
110110

111+
#### Controlling how connections are kept statically in the current php process
112+
113+
By default, every configured doctrine DBAL connection will have its own driver connection that is managed in the current php process.
114+
In case you need to customize this behavior you can choose different "connection keys" that are used to select driver connections.
115+
116+
Example for 2 connections that will re-use the same driver connection instance:
117+
118+
```yaml
119+
doctrine:
120+
dbal:
121+
connections:
122+
default:
123+
url: '%database.url1%'
124+
125+
foo:
126+
url: '%database.url2%'
127+
128+
dama_doctrine_test:
129+
connection_keys:
130+
# assigning the same key will result in the same internal driver connection being re-used for both DBAL connections
131+
default: custom_key
132+
foo: custom_key
133+
```
134+
135+
**Since v8.1.0**: For connections with read/write replicas the bundle will use the **same** underlying driver connection by default for the primary and also for replicas. This addresses an [issue](https://github.com/dmaicher/doctrine-test-bundle/issues/289) where inconsistencies happened when reading/writing to different connections. This can also be customized as follows:
136+
137+
```yaml
138+
doctrine:
139+
dbal:
140+
connections:
141+
default:
142+
url: '%database.url%'
143+
replicas:
144+
replica_one:
145+
url: '%database.url_replica%'
146+
147+
dama_doctrine_test:
148+
connection_keys:
149+
# assigning different keys will result in separate internal driver connection being used for primary and replica
150+
default:
151+
primary: custom_key_primary
152+
replicas:
153+
replica_one: custom_key_primary_replica
154+
```
155+
111156
### Example
112157

113158
An example usage can be seen within the functional tests included in this bundle: https://github.com/dmaicher/doctrine-test-bundle/tree/master/tests

composer.json

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
],
2020
"require": {
2121
"php": "^7.4 || ^8.0",
22-
"ext-json": "*",
2322
"doctrine/dbal": "^3.3 || ^4.0",
2423
"doctrine/doctrine-bundle": "^2.2.2",
2524
"psr/cache": "^1.0 || ^2.0 || ^3.0",

makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ test_phpunit_legacy: tests/Functional/app/parameters.yml
88
vendor/bin/phpunit -c tests/phpunit.9.xml tests/
99

1010
phpstan:
11-
vendor/bin/phpstan analyse -c phpstan.neon -a vendor/autoload.php -l 5 src tests
11+
vendor/bin/phpstan analyse -c phpstan.neon -a vendor/autoload.php -l 8 src
12+
vendor/bin/phpstan analyse -c phpstan.neon -a vendor/autoload.php -l 5 tests
1213

1314
behat:
1415
vendor/bin/behat -c tests/behat.yml -fprogress

src/DAMA/DoctrineTestBundle/DependencyInjection/Configuration.php

+49
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class Configuration implements ConfigurationInterface
99
{
1010
public const ENABLE_STATIC_CONNECTION = 'enable_static_connection';
11+
public const CONNECTION_KEYS = 'connection_keys';
1112
public const STATIC_META_CACHE = 'enable_static_meta_data_cache';
1213
public const STATIC_QUERY_CACHE = 'enable_static_query_cache';
1314

@@ -45,9 +46,57 @@ public function getConfigTreeBuilder(): TreeBuilder
4546
->end()
4647
->booleanNode(self::STATIC_META_CACHE)->defaultTrue()->end()
4748
->booleanNode(self::STATIC_QUERY_CACHE)->defaultTrue()->end()
49+
->arrayNode(self::CONNECTION_KEYS)
50+
->normalizeKeys(false)
51+
->variablePrototype()
52+
->end()
53+
->validate()
54+
->ifTrue(function ($value) {
55+
if ($value === null) {
56+
return false;
57+
}
58+
59+
if (!is_array($value)) {
60+
return true;
61+
}
62+
63+
foreach ($value as $k => $v) {
64+
if (!is_string($k) || !(is_string($v) || is_array($v))) {
65+
return true;
66+
}
67+
68+
if (!is_array($v)) {
69+
continue;
70+
}
71+
72+
if (count($v) !== 2
73+
|| !is_string($v['primary'] ?? null)
74+
|| !is_array($v['replicas'] ?? null)
75+
|| !$this->isAssocStringArray($v['replicas'])
76+
) {
77+
return true;
78+
}
79+
}
80+
81+
return false;
82+
})
83+
->thenInvalid('Must be array<string, string|array{primary: string, replicas: array<string, string>}>')
84+
->end()
85+
->end()
4886
->end()
4987
;
5088

5189
return $treeBuilder;
5290
}
91+
92+
private function isAssocStringArray(array $value): bool
93+
{
94+
foreach ($value as $k => $v) {
95+
if (!is_string($k) || !is_string($v)) {
96+
return false;
97+
}
98+
}
99+
100+
return true;
101+
}
53102
}

src/DAMA/DoctrineTestBundle/DependencyInjection/DAMADoctrineTestExtension.php

+4
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@ public function load(array $configs, ContainerBuilder $container): void
2424
'dama.'.Configuration::ENABLE_STATIC_CONNECTION,
2525
$config[Configuration::ENABLE_STATIC_CONNECTION]
2626
);
27+
$container->setParameter(
28+
'dama.'.Configuration::CONNECTION_KEYS,
29+
$config[Configuration::CONNECTION_KEYS] ?? [],
30+
);
2731
}
2832
}

src/DAMA/DoctrineTestBundle/DependencyInjection/DoctrineTestCompilerPass.php

+72-30
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class DoctrineTestCompilerPass implements CompilerPassInterface
1717
{
1818
public function process(ContainerBuilder $container): void
1919
{
20-
$transactionalBehaviorEnabledConnections = $this->getTransactionEnabledConnectionNames($container);
2120
$container->register('dama.doctrine.dbal.middleware', Middleware::class);
2221
$cacheNames = [];
2322

@@ -29,11 +28,15 @@ public function process(ContainerBuilder $container): void
2928
$cacheNames[] = 'doctrine.orm.%s_query_cache';
3029
}
3130

32-
$connectionNames = array_keys($container->getParameter('doctrine.connections'));
31+
/** @var array<string, mixed> $connections */
32+
$connections = $container->getParameter('doctrine.connections');
33+
$connectionNames = array_keys($connections);
34+
$transactionalBehaviorEnabledConnections = $this->getTransactionEnabledConnectionNames($container, $connectionNames);
35+
$connectionKeys = $this->getConnectionKeys($container, $connectionNames);
3336

3437
foreach ($connectionNames as $name) {
3538
if (in_array($name, $transactionalBehaviorEnabledConnections, true)) {
36-
$this->modifyConnectionService($container, $name);
39+
$this->modifyConnectionService($container, $connectionKeys[$name] ?? null, $name);
3740
}
3841

3942
foreach ($cacheNames as $cacheName) {
@@ -56,19 +59,25 @@ public function process(ContainerBuilder $container): void
5659
$container->getParameterBag()->remove('dama.'.Configuration::ENABLE_STATIC_CONNECTION);
5760
$container->getParameterBag()->remove('dama.'.Configuration::STATIC_META_CACHE);
5861
$container->getParameterBag()->remove('dama.'.Configuration::STATIC_QUERY_CACHE);
62+
$container->getParameterBag()->remove('dama.'.Configuration::CONNECTION_KEYS);
5963
}
6064

61-
private function modifyConnectionService(ContainerBuilder $container, string $name): void
65+
/**
66+
* @param string|array{primary: string, replicas: array<string, string>}|null $connectionKey
67+
*/
68+
private function modifyConnectionService(ContainerBuilder $container, $connectionKey, string $name): void
6269
{
6370
$connectionDefinition = $container->getDefinition(sprintf('doctrine.dbal.%s_connection', $name));
6471

6572
if (!$this->hasSavepointsEnabled($connectionDefinition)) {
6673
throw new \LogicException(sprintf('This bundle relies on savepoints for nested database transactions. You need to enable "use_savepoints" on the Doctrine DBAL config for connection "%s".', $name));
6774
}
6875

76+
/** @var array<string, mixed> $connectionOptions */
77+
$connectionOptions = $connectionDefinition->getArgument(0);
6978
$connectionDefinition->replaceArgument(
7079
0,
71-
$this->getModifiedConnectionOptions($connectionDefinition->getArgument(0), $name),
80+
$this->getModifiedConnectionOptions($connectionOptions, $connectionKey, $name),
7281
);
7382

7483
$connectionConfig = $container->getDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name));
@@ -90,27 +99,44 @@ private function modifyConnectionService(ContainerBuilder $container, string $na
9099
$connectionConfig->setMethodCalls($methodCalls);
91100
}
92101

93-
private function getModifiedConnectionOptions(array $options, string $name): array
94-
{
95-
$connectionOptions = array_merge($options, [
96-
'dama.keep_static' => true,
97-
'dama.connection_name' => $name,
98-
]);
99-
100-
if (isset($connectionOptions['primary'])) {
101-
$connectionOptions['primary'] = array_merge($connectionOptions['primary'], [
102-
'dama.keep_static' => true,
103-
'dama.connection_name' => $name,
104-
]);
105-
}
106-
107-
if (isset($connectionOptions['replica']) && is_array($connectionOptions['replica'])) {
108-
foreach ($connectionOptions['replica'] as $replicaName => &$replica) {
109-
$replica = array_merge($replica, [
110-
'dama.keep_static' => true,
111-
'dama.connection_name' => sprintf('%s.%s', $name, $replicaName),
112-
]);
102+
/**
103+
* @param array<string, mixed> $connectionOptions
104+
* @param string|array{primary: string, replicas: array<string, string>}|null $connectionKey
105+
*
106+
* @return array<string, mixed>
107+
*/
108+
private function getModifiedConnectionOptions(
109+
array $connectionOptions,
110+
$connectionKey,
111+
string $name
112+
): array {
113+
if (!isset($connectionOptions['primary'])) {
114+
if (is_array($connectionKey)) {
115+
throw new \InvalidArgumentException(sprintf('Connection key for connection "%s" must be a string', $name));
113116
}
117+
118+
$connectionOptions['dama.connection_key'] = $connectionKey ?? $name;
119+
120+
return $connectionOptions;
121+
}
122+
123+
$connectionOptions['dama.connection_key'] = $connectionKey['primary'] ?? $connectionKey ?? $name;
124+
$connectionOptions['primary']['dama.connection_key'] = $connectionOptions['dama.connection_key'];
125+
126+
if (!is_array($connectionOptions['replica'] ?? null)) {
127+
return $connectionOptions;
128+
}
129+
130+
$replicaKeys = [];
131+
if (isset($connectionKey['replicas'])) {
132+
/** @var array<string> $definedReplicaNames */
133+
$definedReplicaNames = array_keys($connectionOptions['replica']);
134+
$this->validateConnectionNames(array_keys($connectionKey['replicas']), $definedReplicaNames);
135+
$replicaKeys = $connectionKey['replicas'];
136+
}
137+
138+
foreach ($connectionOptions['replica'] as $replicaName => &$replicaOptions) {
139+
$replicaOptions['dama.connection_key'] = $replicaKeys[$replicaName] ?? $connectionOptions['dama.connection_key'];
114140
}
115141

116142
return $connectionOptions;
@@ -123,11 +149,12 @@ private function registerStaticCache(
123149
): void {
124150
$cache = new Definition();
125151
$namespace = sha1($cacheServiceId);
152+
$originalServiceClass = $originalCacheServiceDefinition->getClass();
126153

127-
if (is_a($originalCacheServiceDefinition->getClass(), CacheItemPoolInterface::class, true)) {
154+
if ($originalServiceClass !== null && is_a($originalServiceClass, CacheItemPoolInterface::class, true)) {
128155
$cache->setClass(Psr6StaticArrayCache::class);
129156
$cache->setArgument(0, $namespace); // make sure we have no key collisions
130-
} elseif (is_a($originalCacheServiceDefinition->getClass(), Cache::class, true)) {
157+
} elseif ($originalServiceClass !== null && is_a($originalServiceClass, Cache::class, true)) {
131158
throw new \InvalidArgumentException(sprintf('Configuring "%s" caches is not supported anymore. Upgrade to PSR-6 caches instead.', Cache::class));
132159
} else {
133160
throw new \InvalidArgumentException(sprintf('Unsupported cache class "%s" found on service "%s".', $originalCacheServiceDefinition->getClass(), $cacheServiceId));
@@ -140,14 +167,15 @@ private function registerStaticCache(
140167
}
141168

142169
/**
170+
* @param string[] $connectionNames
171+
*
143172
* @return string[]
144173
*/
145-
private function getTransactionEnabledConnectionNames(ContainerBuilder $container): array
174+
private function getTransactionEnabledConnectionNames(ContainerBuilder $container, array $connectionNames): array
146175
{
147-
/** @var bool|array $enableStaticConnectionsConfig */
176+
/** @var bool|array<string, bool> $enableStaticConnectionsConfig */
148177
$enableStaticConnectionsConfig = $container->getParameter('dama.'.Configuration::ENABLE_STATIC_CONNECTION);
149178

150-
$connectionNames = array_keys($container->getParameter('doctrine.connections'));
151179
if (is_array($enableStaticConnectionsConfig)) {
152180
$this->validateConnectionNames(array_keys($enableStaticConnectionsConfig), $connectionNames);
153181
}
@@ -193,4 +221,18 @@ private function hasSavepointsEnabled(Definition $connectionDefinition): bool
193221

194222
return false;
195223
}
224+
225+
/**
226+
* @param string[] $connectionNames
227+
*
228+
* @return array<string, string|array{primary: string, replicas: array<string, string>}>
229+
*/
230+
private function getConnectionKeys(ContainerBuilder $container, array $connectionNames): array
231+
{
232+
/** @var array<string, string> $connectionKeys */
233+
$connectionKeys = $container->getParameter('dama.'.Configuration::CONNECTION_KEYS);
234+
$this->validateConnectionNames(array_keys($connectionKeys), $connectionNames);
235+
236+
return $connectionKeys;
237+
}
196238
}

src/DAMA/DoctrineTestBundle/Doctrine/Cache/Psr6StaticArrayCache.php

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public function getItem($key): CacheItemInterface
3939
return $this->adapter->getItem($key);
4040
}
4141

42+
/**
43+
* @return iterable<CacheItemInterface>
44+
*/
4245
public function getItems(array $keys = []): iterable
4346
{
4447
return $this->adapter->getItems($keys);

src/DAMA/DoctrineTestBundle/Doctrine/DBAL/StaticDriver.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class StaticDriver extends Driver\Middleware\AbstractDriverMiddleware
1111
{
1212
/**
13-
* @var Connection[]
13+
* @var array<string, Connection>
1414
*/
1515
private static $connections = [];
1616

@@ -21,14 +21,12 @@ class StaticDriver extends Driver\Middleware\AbstractDriverMiddleware
2121

2222
public function connect(array $params): Connection
2323
{
24-
if (!self::isKeepStaticConnections()
25-
|| !isset($params['dama.keep_static'])
26-
|| !$params['dama.keep_static']
27-
) {
24+
if (!self::isKeepStaticConnections() || !isset($params['dama.connection_key'])) {
2825
return parent::connect($params);
2926
}
3027

31-
$key = sha1(json_encode($params));
28+
/** @var string $key */
29+
$key = $params['dama.connection_key'];
3230

3331
if (!isset(self::$connections[$key])) {
3432
self::$connections[$key] = parent::connect($params);

0 commit comments

Comments
 (0)