diff --git a/.github/workflows/test-dev-stability.yml b/.github/workflows/test-dev-stability.yml index 50a35528c..ef3df1eee 100644 --- a/.github/workflows/test-dev-stability.yml +++ b/.github/workflows/test-dev-stability.yml @@ -52,4 +52,4 @@ jobs: composer-options: "--prefer-dist" - name: "Run PHPUnit" - run: "SYMFONY_DEPRECATIONS_HELPER=${{env.GITHUB_WORKSPACE}}/tests/baseline-ignore vendor/bin/phpunit" + run: "vendor/bin/phpunit" diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index fbef242fb..0e47f0432 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\Exception\LogicException; +use function array_diff_key; use function array_intersect_key; use function array_key_exists; use function array_keys; @@ -72,17 +73,27 @@ public function getConfigTreeBuilder(): TreeBuilder */ private function addDbalSection(ArrayNodeDefinition $node): void { + // Key that should not be rewritten to the connection config + $excludedKeys = ['default_connection' => true, 'types' => true, 'type' => true]; + $node ->children() ->arrayNode('dbal') ->beforeNormalization() - ->ifTrue(static function ($v) { - return is_array($v) && ! array_key_exists('connections', $v) && ! array_key_exists('connection', $v); + ->ifTrue(static function ($v) use ($excludedKeys) { + if (! is_array($v)) { + return false; + } + + if (array_key_exists('connections', $v) || array_key_exists('connection', $v)) { + return false; + } + + // Is there actually anything to use once excluded keys are considered? + return (bool) array_diff_key($v, $excludedKeys); }) - ->then(static function ($v) { - // Key that should not be rewritten to the connection config - $excludedKeys = ['default_connection' => true, 'types' => true, 'type' => true]; - $connection = []; + ->then(static function ($v) use ($excludedKeys) { + $connection = []; foreach ($v as $key => $value) { if (isset($excludedKeys[$key])) { continue; @@ -92,8 +103,7 @@ private function addDbalSection(ArrayNodeDefinition $node): void unset($v[$key]); } - $v['default_connection'] = isset($v['default_connection']) ? (string) $v['default_connection'] : 'default'; - $v['connections'] = [$v['default_connection'] => $connection]; + $v['connections'] = [($v['default_connection'] ?? 'default') => $connection]; return $v; }) @@ -380,30 +390,39 @@ private function configureDbalDriverNode(ArrayNodeDefinition $node): void */ private function addOrmSection(ArrayNodeDefinition $node): void { + // Key that should not be rewritten to the entity-manager config + $excludedKeys = [ + 'default_entity_manager' => true, + 'auto_generate_proxy_classes' => true, + 'enable_lazy_ghost_objects' => true, + 'proxy_dir' => true, + 'proxy_namespace' => true, + 'resolve_target_entities' => true, + 'resolve_target_entity' => true, + 'controller_resolver' => true, + ]; + $node ->children() ->arrayNode('orm') ->beforeNormalization() - ->ifTrue(static function ($v) { + ->ifTrue(static function ($v) use ($excludedKeys) { if (! empty($v) && ! class_exists(EntityManager::class)) { throw new LogicException('The doctrine/orm package is required when the doctrine.orm config is set.'); } - return $v === null || (is_array($v) && ! array_key_exists('entity_managers', $v) && ! array_key_exists('entity_manager', $v)); + if (! is_array($v)) { + return false; + } + + if (array_key_exists('entity_managers', $v) || array_key_exists('entity_manager', $v)) { + return false; + } + + // Is there actually anything to use once excluded keys are considered? + return (bool) array_diff_key($v, $excludedKeys); }) - ->then(static function ($v) { - $v = (array) $v; - // Key that should not be rewritten to the entity-manager config - $excludedKeys = [ - 'default_entity_manager' => true, - 'auto_generate_proxy_classes' => true, - 'enable_lazy_ghost_objects' => true, - 'proxy_dir' => true, - 'proxy_namespace' => true, - 'resolve_target_entities' => true, - 'resolve_target_entity' => true, - 'controller_resolver' => true, - ]; + ->then(static function ($v) use ($excludedKeys) { $entityManager = []; foreach ($v as $key => $value) { if (isset($excludedKeys[$key])) { @@ -414,8 +433,7 @@ private function addOrmSection(ArrayNodeDefinition $node): void unset($v[$key]); } - $v['default_entity_manager'] = isset($v['default_entity_manager']) ? (string) $v['default_entity_manager'] : 'default'; - $v['entity_managers'] = [$v['default_entity_manager'] => $entityManager]; + $v['entity_managers'] = [($v['default_entity_manager'] ?? 'default') => $entityManager]; return $v; }) diff --git a/DependencyInjection/DoctrineExtension.php b/DependencyInjection/DoctrineExtension.php index 399cd2f42..b1e39cdb7 100644 --- a/DependencyInjection/DoctrineExtension.php +++ b/DependencyInjection/DoctrineExtension.php @@ -46,6 +46,7 @@ use Symfony\Bridge\Doctrine\Validator\DoctrineLoader; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -63,6 +64,7 @@ use function array_intersect_key; use function array_keys; +use function array_merge; use function class_exists; use function interface_exists; use function is_dir; @@ -89,7 +91,7 @@ class DoctrineExtension extends AbstractDoctrineExtension public function load(array $configs, ContainerBuilder $container) { $configuration = $this->getConfiguration($configs, $container); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfigurationPrependingDefaults($configuration, $configs); if (! empty($config['dbal'])) { $this->dbalLoad($config['dbal'], $container); @@ -108,6 +110,40 @@ public function load(array $configs, ContainerBuilder $container) $this->ormLoad($config['orm'], $container); } + /** + * Process user configuration and adds a default DBAL connection and/or a + * default EM if required, then process again the configuration to get + * default values for each. + * + * @param array> $configs + * + * @return array + */ + private function processConfigurationPrependingDefaults(ConfigurationInterface $configuration, array $configs): array + { + $config = $this->processConfiguration($configuration, $configs); + $configToAdd = []; + + // if no DB connection defined, prepend an empty one for the default + // connection name in order to make Symfony Config resolve the default + // values + if (isset($config['dbal']) && empty($config['dbal']['connections'])) { + $configToAdd['dbal'] = ['connections' => [($config['dbal']['default_connection'] ?? 'default') => []]]; + } + + // if no EM defined, prepend an empty one for the default EM name in + // order to make Symfony Config resolve the default values + if (isset($config['orm']) && empty($config['orm']['entity_managers'])) { + $configToAdd['orm'] = ['entity_managers' => [($config['orm']['default_entity_manager'] ?? 'default') => []]]; + } + + if (! $configToAdd) { + return $config; + } + + return $this->processConfiguration($configuration, array_merge([$configToAdd], $configs)); + } + /** * Loads the DBAL configuration. * diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index f4bc0c18d..a96897086 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -10,4 +10,5 @@ configuration options, console commands and even a web debug toolbar collector. entity-listeners event-listeners custom-id-generators + middlewares configuration diff --git a/Resources/doc/middlewares.rst b/Resources/doc/middlewares.rst new file mode 100644 index 000000000..a71e6ff61 --- /dev/null +++ b/Resources/doc/middlewares.rst @@ -0,0 +1,111 @@ +Middlewares +=========== + +Doctrine DBAL supports middlewares. According to the `DBAL documentation`_: + + "A middleware sits in the middle between the wrapper components and the driver" + +They allow to decorate the following DBAL classes: + +- ``Doctrine\DBAL\Driver`` +- ``Doctrine\DBAL\Driver\Connection`` +- ``Doctrine\DBAL\Driver\Statement`` +- ``Doctrine\DBAL\Driver\Result`` + +Symfony, for instance, uses a middleware to harvest the queries executed +by the current page and make them available in the profiler. + +.. _`DBAL documentation`: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/architecture.html#middlewares + +You can also create your own middleware. This is an example of a (very) +simple middleware that prevents database connections with the root user. +The first step is to create the middleware: + +.. code-block:: php + + assertEquals('foo', $container->getParameter('doctrine.default_connection'), '->load() overrides existing configuration options'); } + public function testDbalOverrideDefaultConnectionWithAdditionalConfiguration(): void + { + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $container->registerExtension($extension); + + $extension->load([['dbal' => ['default_connection' => 'foo']], ['dbal' => ['types' => ['foo' => 'App\\Doctrine\\FooType']]]], $container); + + // doctrine.dbal.default_connection + $this->assertEquals('%doctrine.default_connection%', $container->getDefinition('doctrine')->getArgument(3), '->load() overrides existing configuration options'); + $this->assertEquals('foo', $container->getParameter('doctrine.default_connection'), '->load() overrides existing configuration options'); + } + public function testOrmRequiresDbal(): void { if (! interface_exists(EntityManagerInterface::class)) { @@ -586,6 +600,29 @@ public function testSingleEntityManagerWithDefaultConfiguration(): void ]); } + /** + * @testWith [[]] + * [null] + */ + public function testSingleEntityManagerWithEmptyConfiguration(?array $ormConfiguration): void + { + if (! interface_exists(EntityManagerInterface::class)) { + self::markTestSkipped('This test requires ORM'); + } + + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $extension->load([ + [ + 'dbal' => [], + 'orm' => $ormConfiguration, + ], + ], $container); + + $this->assertEquals('default', $container->getParameter('doctrine.default_entity_manager')); + } + public function testSingleEntityManagerWithDefaultSecondLevelCacheConfiguration(): void { if (! interface_exists(EntityManagerInterface::class)) { @@ -695,6 +732,23 @@ public function testOverwriteEntityAliases(): void ); } + public function testOverrideDefaultEntityManagerWithAdditionalConfiguration(): void + { + if (! interface_exists(EntityManagerInterface::class)) { + self::markTestSkipped('This test requires ORM'); + } + + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $extension->load([ + ['dbal' => [], 'orm' => ['default_entity_manager' => 'app', 'entity_managers' => ['app' => ['mappings' => ['YamlBundle' => ['alias' => 'yml']]]]]], + ['orm' => ['metadata_cache_driver' => ['type' => 'pool', 'pool' => 'doctrine.system_cache_pool']]], + ], $container); + + $this->assertEquals('app', $container->getParameter('doctrine.default_entity_manager')); + } + public function testYamlBundleMappingDetection(): void { if (! interface_exists(EntityManagerInterface::class)) { diff --git a/Tests/baseline-ignore b/Tests/baseline-ignore index 63da562e2..7ec032ab8 100644 --- a/Tests/baseline-ignore +++ b/Tests/baseline-ignore @@ -1,2 +1 @@ -%Method "ArrayAccess::(offsetExists|offsetGet|offsetSet|offsetUnset)+\(\)" might add "\w+" as a native return type declaration in the future. Do the same in implementation "Doctrine\\\Common\\Collections\\ArrayCollection" now to avoid errors or add an explicit @return annotation to suppress this message\.% -%Method "Countable::count\(\)" might add "int" as a native return type declaration in the future. Do the same in implementation "Doctrine\\Common\\Collections\\ArrayCollection" now to avoid errors or add an explicit @return annotation to suppress this message\.% \ No newline at end of file +%Using XML mapping driver with XSD validation disabled is deprecated% \ No newline at end of file