├── LICENSE ├── composer.json └── src └── DAMA └── DoctrineTestBundle ├── Behat ├── BehatListener.php └── ServiceContainer │ └── DoctrineExtension.php ├── DAMADoctrineTestBundle.php ├── DependencyInjection ├── AddMiddlewaresCompilerPass.php ├── Configuration.php ├── DAMADoctrineTestExtension.php └── ModifyDoctrineConfigCompilerPass.php ├── Doctrine ├── Cache │ └── Psr6StaticArrayCache.php └── DBAL │ ├── Middleware.php │ ├── StaticConnection.php │ ├── StaticConnectionTrait.php │ └── StaticDriver.php └── PHPUnit └── PHPUnitExtension.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Maicher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dama/doctrine-test-bundle", 3 | "description": "Symfony bundle to isolate doctrine database tests and improve test performance", 4 | "keywords": [ 5 | "symfony", 6 | "doctrine", 7 | "tests", 8 | "testing", 9 | "isolation", 10 | "performance" 11 | ], 12 | "type": "symfony-bundle", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "David Maicher", 17 | "email": "mail@dmaicher.de" 18 | } 19 | ], 20 | "require": { 21 | "php": ">= 8.1", 22 | "doctrine/dbal": "^3.3 || ^4.0", 23 | "doctrine/doctrine-bundle": "^2.11.0", 24 | "psr/cache": "^2.0 || ^3.0", 25 | "symfony/cache": "^6.4 || ^7.2", 26 | "symfony/framework-bundle": "^6.4 || ^7.2" 27 | }, 28 | "require-dev": { 29 | "behat/behat": "^3.0", 30 | "friendsofphp/php-cs-fixer": "^3.27", 31 | "phpstan/phpstan": "^2.0", 32 | "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", 33 | "symfony/process": "^6.4 || ^7.2", 34 | "symfony/yaml": "^6.4 || ^7.2" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "DAMA\\DoctrineTestBundle\\": "src/DAMA/DoctrineTestBundle" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests/" 44 | } 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "8.x-dev" 49 | } 50 | }, 51 | "config": { 52 | "sort-packages": true 53 | }, 54 | "conflict": { 55 | "phpunit/phpunit": "<10.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Behat/BehatListener.php: -------------------------------------------------------------------------------- 1 | 'enableStaticConnection', 17 | ExerciseCompleted::AFTER => 'disableStaticConnection', 18 | ScenarioTested::BEFORE => ['beginTransaction', 255], 19 | ExampleTested::BEFORE => ['beginTransaction', 255], 20 | ScenarioTested::AFTER => ['rollBack', -255], 21 | ExampleTested::AFTER => ['rollBack', -255], 22 | ]; 23 | } 24 | 25 | public function enableStaticConnection(): void 26 | { 27 | StaticDriver::setKeepStaticConnections(true); 28 | } 29 | 30 | public function disableStaticConnection(): void 31 | { 32 | StaticDriver::setKeepStaticConnections(false); 33 | } 34 | 35 | public function beginTransaction(): void 36 | { 37 | StaticDriver::beginTransaction(); 38 | } 39 | 40 | public function rollBack(): void 41 | { 42 | StaticDriver::rollBack(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Behat/ServiceContainer/DoctrineExtension.php: -------------------------------------------------------------------------------- 1 | register('dama_doctrine_test.listener', BehatListener::class) 30 | ->addTag(EventDispatcherExtension::SUBSCRIBER_TAG) 31 | ; 32 | } 33 | 34 | public function process(ContainerBuilder $container): void 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/DAMADoctrineTestBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new ModifyDoctrineConfigCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1); 18 | 19 | // higher priority than MiddlewaresPass from DoctrineBundle 20 | $container->addCompilerPass(new AddMiddlewaresCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/DependencyInjection/AddMiddlewaresCompilerPass.php: -------------------------------------------------------------------------------- 1 | $connections */ 18 | $connections = $container->getParameter('doctrine.connections'); 19 | $connectionNames = array_keys($connections); 20 | $transactionalBehaviorEnabledConnections = $this->getTransactionEnabledConnectionNames($container, $connectionNames); 21 | $container->getParameterBag()->set(self::TRANSACTIONAL_BEHAVIOR_ENABLED_CONNECTIONS, $transactionalBehaviorEnabledConnections); 22 | 23 | foreach ($transactionalBehaviorEnabledConnections as $name) { 24 | $middlewareDefinition = $container->register(sprintf('dama.doctrine.dbal.middleware.%s', $name), Middleware::class); 25 | $middlewareDefinition->addTag('doctrine.middleware', ['connection' => $name, 'priority' => 100]); 26 | } 27 | 28 | $container->getParameterBag()->remove('dama.'.Configuration::ENABLE_STATIC_CONNECTION); 29 | } 30 | 31 | /** 32 | * @param string[] $connectionNames 33 | * 34 | * @return string[] 35 | */ 36 | private function getTransactionEnabledConnectionNames(ContainerBuilder $container, array $connectionNames): array 37 | { 38 | /** @var bool|array $enableStaticConnectionsConfig */ 39 | $enableStaticConnectionsConfig = $container->getParameter('dama.'.Configuration::ENABLE_STATIC_CONNECTION); 40 | 41 | if (is_array($enableStaticConnectionsConfig)) { 42 | $this->validateConnectionNames(array_keys($enableStaticConnectionsConfig), $connectionNames); 43 | } 44 | 45 | $enabledConnections = []; 46 | 47 | foreach ($connectionNames as $name) { 48 | if ($enableStaticConnectionsConfig === true 49 | || isset($enableStaticConnectionsConfig[$name]) && $enableStaticConnectionsConfig[$name] === true 50 | ) { 51 | $enabledConnections[] = $name; 52 | } 53 | } 54 | 55 | return $enabledConnections; 56 | } 57 | 58 | /** 59 | * @param string[] $configNames 60 | * @param string[] $existingNames 61 | */ 62 | private function validateConnectionNames(array $configNames, array $existingNames): void 63 | { 64 | $unknown = array_diff($configNames, $existingNames); 65 | 66 | if (count($unknown)) { 67 | throw new \InvalidArgumentException(sprintf('Unknown doctrine dbal connection name(s): %s.', implode(', ', $unknown))); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 20 | 21 | $root 22 | ->addDefaultsIfNotSet() 23 | ->children() 24 | ->variableNode(self::ENABLE_STATIC_CONNECTION) 25 | ->defaultTrue() 26 | ->validate() 27 | ->ifTrue(function ($value) { 28 | if (is_bool($value)) { 29 | return false; 30 | } 31 | 32 | if (!is_array($value)) { 33 | return true; 34 | } 35 | 36 | foreach ($value as $k => $v) { 37 | if (!is_string($k) || !is_bool($v)) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | }) 44 | ->thenInvalid('Must be a boolean or an array with name -> bool') 45 | ->end() 46 | ->end() 47 | ->booleanNode(self::STATIC_META_CACHE)->defaultTrue()->end() 48 | ->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}>') 84 | ->end() 85 | ->end() 86 | ->end() 87 | ; 88 | 89 | return $treeBuilder; 90 | } 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 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/DependencyInjection/DAMADoctrineTestExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 14 | 15 | $container->setParameter( 16 | 'dama.'.Configuration::STATIC_META_CACHE, 17 | (bool) $config[Configuration::STATIC_META_CACHE] 18 | ); 19 | $container->setParameter( 20 | 'dama.'.Configuration::STATIC_QUERY_CACHE, 21 | (bool) $config[Configuration::STATIC_QUERY_CACHE] 22 | ); 23 | $container->setParameter( 24 | 'dama.'.Configuration::ENABLE_STATIC_CONNECTION, 25 | $config[Configuration::ENABLE_STATIC_CONNECTION] 26 | ); 27 | $container->setParameter( 28 | 'dama.'.Configuration::CONNECTION_KEYS, 29 | $config[Configuration::CONNECTION_KEYS] ?? [], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/DependencyInjection/ModifyDoctrineConfigCompilerPass.php: -------------------------------------------------------------------------------- 1 | getParameter('dama.'.Configuration::STATIC_META_CACHE)) { 21 | $cacheNames[] = 'doctrine.orm.%s_metadata_cache'; 22 | } 23 | 24 | if ($container->getParameter('dama.'.Configuration::STATIC_QUERY_CACHE)) { 25 | $cacheNames[] = 'doctrine.orm.%s_query_cache'; 26 | } 27 | 28 | /** @var array $connections */ 29 | $connections = $container->getParameter('doctrine.connections'); 30 | $connectionNames = array_keys($connections); 31 | 32 | /** @var string[] $transactionalBehaviorEnabledConnections */ 33 | $transactionalBehaviorEnabledConnections = $container->getParameter( 34 | AddMiddlewaresCompilerPass::TRANSACTIONAL_BEHAVIOR_ENABLED_CONNECTIONS, 35 | ); 36 | $connectionKeys = $this->getConnectionKeys($container, $connectionNames); 37 | 38 | foreach ($connectionNames as $name) { 39 | if (in_array($name, $transactionalBehaviorEnabledConnections, true)) { 40 | $this->modifyConnectionService($container, $connectionKeys[$name] ?? null, $name); 41 | } 42 | 43 | foreach ($cacheNames as $cacheName) { 44 | $cacheServiceId = sprintf($cacheName, $name); 45 | 46 | if (!$container->has($cacheServiceId)) { 47 | // might happen if ORM is not used 48 | continue; 49 | } 50 | 51 | $definition = $container->findDefinition($cacheServiceId); 52 | while (!$definition->getClass() && $definition instanceof ChildDefinition) { 53 | $definition = $container->findDefinition($definition->getParent()); 54 | } 55 | 56 | $this->registerStaticCache($container, $definition, $cacheServiceId); 57 | } 58 | } 59 | 60 | $container->getParameterBag()->remove('dama.'.Configuration::STATIC_META_CACHE); 61 | $container->getParameterBag()->remove('dama.'.Configuration::STATIC_QUERY_CACHE); 62 | $container->getParameterBag()->remove('dama.'.Configuration::CONNECTION_KEYS); 63 | $container->getParameterBag()->remove(AddMiddlewaresCompilerPass::TRANSACTIONAL_BEHAVIOR_ENABLED_CONNECTIONS); 64 | } 65 | 66 | /** 67 | * @param string|array{primary: string, replicas: array}|null $connectionKey 68 | */ 69 | private function modifyConnectionService(ContainerBuilder $container, $connectionKey, string $name): void 70 | { 71 | $connectionDefinition = $container->getDefinition(sprintf('doctrine.dbal.%s_connection', $name)); 72 | 73 | if (!$this->hasSavepointsEnabled($connectionDefinition)) { 74 | 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)); 75 | } 76 | 77 | /** @var array $connectionOptions */ 78 | $connectionOptions = $connectionDefinition->getArgument(0); 79 | $connectionDefinition->replaceArgument( 80 | 0, 81 | $this->getModifiedConnectionOptions($connectionOptions, $connectionKey, $name), 82 | ); 83 | } 84 | 85 | /** 86 | * @param array $connectionOptions 87 | * @param string|array{primary: string, replicas: array}|null $connectionKey 88 | * 89 | * @return array 90 | */ 91 | private function getModifiedConnectionOptions( 92 | array $connectionOptions, 93 | $connectionKey, 94 | string $name 95 | ): array { 96 | if (!isset($connectionOptions['primary'])) { 97 | if (is_array($connectionKey)) { 98 | throw new \InvalidArgumentException(sprintf('Connection key for connection "%s" must be a string', $name)); 99 | } 100 | 101 | $connectionOptions['dama.connection_key'] = $connectionKey ?? $name; 102 | 103 | return $connectionOptions; 104 | } 105 | 106 | $connectionOptions['dama.connection_key'] = $connectionKey['primary'] ?? $connectionKey ?? $name; 107 | $connectionOptions['primary']['dama.connection_key'] = $connectionOptions['dama.connection_key']; 108 | 109 | if (!is_array($connectionOptions['replica'] ?? null)) { 110 | return $connectionOptions; 111 | } 112 | 113 | $replicaKeys = []; 114 | if (isset($connectionKey['replicas'])) { 115 | /** @var array $definedReplicaNames */ 116 | $definedReplicaNames = array_keys($connectionOptions['replica']); 117 | $this->validateConnectionNames(array_keys($connectionKey['replicas']), $definedReplicaNames); 118 | $replicaKeys = $connectionKey['replicas']; 119 | } 120 | 121 | foreach ($connectionOptions['replica'] as $replicaName => &$replicaOptions) { 122 | $replicaOptions['dama.connection_key'] = $replicaKeys[$replicaName] ?? $connectionOptions['dama.connection_key']; 123 | } 124 | 125 | return $connectionOptions; 126 | } 127 | 128 | private function registerStaticCache( 129 | ContainerBuilder $container, 130 | Definition $originalCacheServiceDefinition, 131 | string $cacheServiceId 132 | ): void { 133 | $cache = new Definition(); 134 | $namespace = sha1($cacheServiceId); 135 | $originalServiceClass = $originalCacheServiceDefinition->getClass(); 136 | 137 | if ($originalServiceClass !== null && is_a($originalServiceClass, CacheItemPoolInterface::class, true)) { 138 | $cache->setClass(Psr6StaticArrayCache::class); 139 | $cache->setArgument(0, $namespace); // make sure we have no key collisions 140 | } elseif ($originalServiceClass !== null && is_a($originalServiceClass, Cache::class, true)) { 141 | throw new \InvalidArgumentException(sprintf('Configuring "%s" caches is not supported anymore. Upgrade to PSR-6 caches instead.', Cache::class)); 142 | } else { 143 | throw new \InvalidArgumentException(sprintf('Unsupported cache class "%s" found on service "%s".', $originalCacheServiceDefinition->getClass(), $cacheServiceId)); 144 | } 145 | 146 | if ($container->hasAlias($cacheServiceId)) { 147 | $container->removeAlias($cacheServiceId); 148 | } 149 | $container->setDefinition($cacheServiceId, $cache); 150 | } 151 | 152 | /** 153 | * @param string[] $configNames 154 | * @param string[] $existingNames 155 | */ 156 | private function validateConnectionNames(array $configNames, array $existingNames): void 157 | { 158 | $unknown = array_diff($configNames, $existingNames); 159 | 160 | if (count($unknown)) { 161 | throw new \InvalidArgumentException(sprintf('Unknown doctrine dbal connection name(s): %s.', implode(', ', $unknown))); 162 | } 163 | } 164 | 165 | private function hasSavepointsEnabled(Definition $connectionDefinition): bool 166 | { 167 | // DBAL 4 implicitly always enables savepoints 168 | if (!method_exists(Connection::class, 'getEventManager')) { 169 | return true; 170 | } 171 | 172 | foreach ($connectionDefinition->getMethodCalls() as $call) { 173 | if ($call[0] === 'setNestTransactionsWithSavepoints' && isset($call[1][0]) && $call[1][0]) { 174 | return true; 175 | } 176 | } 177 | 178 | return false; 179 | } 180 | 181 | /** 182 | * @param string[] $connectionNames 183 | * 184 | * @return array}> 185 | */ 186 | private function getConnectionKeys(ContainerBuilder $container, array $connectionNames): array 187 | { 188 | /** @var array $connectionKeys */ 189 | $connectionKeys = $container->getParameter('dama.'.Configuration::CONNECTION_KEYS); 190 | $this->validateConnectionNames(array_keys($connectionKeys), $connectionNames); 191 | 192 | return $connectionKeys; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Doctrine/Cache/Psr6StaticArrayCache.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private static $adaptersByNamespace; 15 | 16 | /** 17 | * @var ArrayAdapter 18 | */ 19 | private $adapter; 20 | 21 | public function __construct(string $namespace) 22 | { 23 | if (!isset(self::$adaptersByNamespace[$namespace])) { 24 | self::$adaptersByNamespace[$namespace] = new ArrayAdapter(0, false); 25 | } 26 | $this->adapter = self::$adaptersByNamespace[$namespace]; 27 | } 28 | 29 | /** 30 | * @internal 31 | */ 32 | public static function reset(): void 33 | { 34 | self::$adaptersByNamespace = []; 35 | } 36 | 37 | public function getItem($key): CacheItemInterface 38 | { 39 | return $this->adapter->getItem($key); 40 | } 41 | 42 | /** 43 | * @return iterable 44 | */ 45 | public function getItems(array $keys = []): iterable 46 | { 47 | return $this->adapter->getItems($keys); 48 | } 49 | 50 | public function hasItem($key): bool 51 | { 52 | return $this->adapter->hasItem($key); 53 | } 54 | 55 | public function clear(): bool 56 | { 57 | return $this->adapter->clear(); 58 | } 59 | 60 | public function deleteItem($key): bool 61 | { 62 | return $this->adapter->deleteItem($key); 63 | } 64 | 65 | public function deleteItems(array $keys): bool 66 | { 67 | return $this->adapter->deleteItems($keys); 68 | } 69 | 70 | public function save(CacheItemInterface $item): bool 71 | { 72 | return $this->adapter->save($item); 73 | } 74 | 75 | public function saveDeferred(CacheItemInterface $item): bool 76 | { 77 | return $this->adapter->saveDeferred($item); 78 | } 79 | 80 | public function commit(): bool 81 | { 82 | return $this->adapter->commit(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Doctrine/DBAL/Middleware.php: -------------------------------------------------------------------------------- 1 | doBeginTransaction(); 20 | 21 | return true; 22 | } 23 | 24 | public function commit(): bool 25 | { 26 | $this->doCommit(); 27 | 28 | return true; 29 | } 30 | 31 | public function rollBack(): bool 32 | { 33 | $this->doRollBack(); 34 | 35 | return true; 36 | } 37 | } 38 | } else { 39 | // DBAL >= 4 40 | class StaticConnection extends AbstractConnectionMiddleware 41 | { 42 | use StaticConnectionTrait; 43 | 44 | public function beginTransaction(): void 45 | { 46 | $this->doBeginTransaction(); 47 | } 48 | 49 | public function commit(): void 50 | { 51 | $this->doCommit(); 52 | } 53 | 54 | public function rollBack(): void 55 | { 56 | $this->doRollBack(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Doctrine/DBAL/StaticConnectionTrait.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 34 | $this->platform = $platform; 35 | } 36 | 37 | private function doBeginTransaction(): void 38 | { 39 | if ($this->nested) { 40 | throw new \BadMethodCallException(sprintf('Bad call to "%s". A savepoint is already in use for a nested transaction.', __METHOD__)); 41 | } 42 | 43 | $this->exec($this->platform->createSavePoint('DAMA_TEST')); 44 | 45 | $this->nested = true; 46 | } 47 | 48 | private function doCommit(): void 49 | { 50 | if (!$this->nested) { 51 | throw new \BadMethodCallException(sprintf('Bad call to "%s". There is no savepoint for a nested transaction.', __METHOD__)); 52 | } 53 | 54 | if ($this->platform->supportsReleaseSavepoints()) { 55 | $this->exec($this->platform->releaseSavePoint('DAMA_TEST')); 56 | } 57 | 58 | $this->nested = false; 59 | } 60 | 61 | private function doRollBack(): void 62 | { 63 | if (!$this->nested) { 64 | throw new \BadMethodCallException(sprintf('Bad call to "%s". There is no savepoint for a nested transaction.', __METHOD__)); 65 | } 66 | 67 | $this->exec($this->platform->rollbackSavePoint('DAMA_TEST')); 68 | 69 | $this->nested = false; 70 | } 71 | 72 | public function getWrappedConnection(): Connection 73 | { 74 | return $this->connection; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/Doctrine/DBAL/StaticDriver.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private static $connections = []; 16 | 17 | /** 18 | * @var bool 19 | */ 20 | private static $keepStaticConnections = false; 21 | 22 | public function connect(array $params): Connection 23 | { 24 | if (!self::isKeepStaticConnections() || !isset($params['dama.connection_key'])) { 25 | return parent::connect($params); 26 | } 27 | 28 | /** @var string $key */ 29 | $key = $params['dama.connection_key']; 30 | 31 | if (!isset(self::$connections[$key])) { 32 | self::$connections[$key] = parent::connect($params); 33 | self::$connections[$key]->beginTransaction(); 34 | } 35 | 36 | $connection = self::$connections[$key]; 37 | 38 | $platform = $this->getPlatform($connection, $params); 39 | 40 | if (!$platform->supportsSavepoints()) { 41 | throw new \RuntimeException('This bundle only works for database platforms that support savepoints.'); 42 | } 43 | 44 | return new StaticConnection($connection, $platform); 45 | } 46 | 47 | public static function setKeepStaticConnections(bool $keepStaticConnections): void 48 | { 49 | self::$keepStaticConnections = $keepStaticConnections; 50 | } 51 | 52 | public static function isKeepStaticConnections(): bool 53 | { 54 | return self::$keepStaticConnections; 55 | } 56 | 57 | public static function beginTransaction(): void 58 | { 59 | foreach (self::$connections as $connection) { 60 | $connection->beginTransaction(); 61 | } 62 | } 63 | 64 | public static function rollBack(): void 65 | { 66 | foreach (self::$connections as $connection) { 67 | $connection->rollBack(); 68 | } 69 | } 70 | 71 | public static function commit(): void 72 | { 73 | foreach (self::$connections as $connection) { 74 | $connection->commit(); 75 | } 76 | } 77 | 78 | private function getPlatform(Connection $connection, array $params): AbstractPlatform 79 | { 80 | if (isset($params['platform'])) { 81 | return $params['platform']; 82 | } 83 | 84 | // DBAL 3 85 | if (method_exists($this, 'createDatabasePlatformForVersion')) { 86 | if (isset($params['serverVersion'])) { 87 | return $this->createDatabasePlatformForVersion($params['serverVersion']); 88 | } 89 | 90 | return $this->getDatabasePlatform(); 91 | } 92 | 93 | // DBAL 4 94 | return $this->getDatabasePlatform( 95 | isset($params['serverVersion']) 96 | ? new StaticServerVersionProvider($params['serverVersion']) 97 | : $connection, 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/DAMA/DoctrineTestBundle/PHPUnit/PHPUnitExtension.php: -------------------------------------------------------------------------------- 1 | registerSubscriber(new class implements TestRunnerStartedSubscriber { 42 | public function notify(TestRunnerStartedEvent $event): void 43 | { 44 | StaticDriver::setKeepStaticConnections(true); 45 | } 46 | }); 47 | 48 | $facade->registerSubscriber(new class implements TestStartedSubscriber { 49 | public function notify(TestStartedEvent $event): void 50 | { 51 | StaticDriver::beginTransaction(); 52 | PHPUnitExtension::$transactionStarted = true; 53 | } 54 | }); 55 | 56 | $facade->registerSubscriber(new class implements SkippedSubscriber { 57 | public function notify(Skipped $event): void 58 | { 59 | // this is a workaround to allow skipping tests within the setUp() method 60 | // as for those cases there is no Finished event 61 | PHPUnitExtension::rollBack(); 62 | } 63 | }); 64 | 65 | $facade->registerSubscriber(new class implements TestFinishedSubscriber { 66 | public function notify(TestFinishedEvent $event): void 67 | { 68 | PHPUnitExtension::rollBack(); 69 | } 70 | }); 71 | 72 | if (interface_exists(BeforeTestMethodErroredSubscriber::class)) { 73 | $facade->registerSubscriber(new class implements BeforeTestMethodErroredSubscriber { 74 | public function notify(BeforeTestMethodErrored $event): void 75 | { 76 | // needed for tests marked incomplete during setUp() 77 | PHPUnitExtension::rollBack(); 78 | } 79 | }); 80 | } 81 | 82 | $facade->registerSubscriber(new class implements ErroredSubscriber { 83 | public function notify(Errored $event): void 84 | { 85 | // needed as for errored tests the "Finished" event is not triggered 86 | PHPUnitExtension::rollBack(); 87 | } 88 | }); 89 | 90 | $facade->registerSubscriber(new class implements TestRunnerFinishedSubscriber { 91 | public function notify(TestRunnerFinishedEvent $event): void 92 | { 93 | StaticDriver::setKeepStaticConnections(false); 94 | } 95 | }); 96 | } 97 | } 98 | --------------------------------------------------------------------------------