Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.9.x' into 2.10.x
Browse files Browse the repository at this point in the history
  • Loading branch information
ostrolucky committed May 24, 2023
2 parents af1df78 + e6da248 commit 2170e49
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-dev-stability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
68 changes: 43 additions & 25 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
})
Expand Down Expand Up @@ -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])) {
Expand All @@ -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;
})
Expand Down
38 changes: 37 additions & 1 deletion DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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<array<mixed>> $configs
*
* @return array<mixed>
*/
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.
*
Expand Down
1 change: 1 addition & 0 deletions Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ configuration options, console commands and even a web debug toolbar collector.
entity-listeners
event-listeners
custom-id-generators
middlewares
configuration
111 changes: 111 additions & 0 deletions Resources/doc/middlewares.rst
Original file line number Diff line number Diff line change
@@ -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
<?php
namespace App\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
class PreventRootConnectionMiddleware implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new PreventRootConnectionDriver($driver);
}
}
As you can see in the ``wrap`` method, the principle of a middleware is
to decorate Doctrine objects with your own objects bearing the logic you
need. Now, the ``connect`` method of the driver must be decorated in
``PreventRootConnectionDriver`` to prevent connections with the root user:

.. code-block:: php
<?php
namespace App\Middleware;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use SensitiveParameter;
final class PreventRootConnectionDriver extends AbstractDriverMiddleware
{
public function connect(array $params): Connection
{
if (isset($params['user']) && $params['user'] === 'root') {
throw new \LogicException('Connecting to the database with the root user is not allowed.');
}
return parent::connect($params);
}
}
That's all! Connection with the root user is not possible anymore. Note
that ``connect`` is not the only method you can decorate in a ``Connection``.
But thanks to the ``AbstractDriverMiddleware`` default implementation,
you only need to decorate the methods for which you want to add some logic.
Too see a more advanced example with a decoration of the ``Statement`` class,
you can look at the middleware implementation starting in the class
``Symfony\Bridge\Doctrine\Middleware\Debug\Middleware`` of the
Doctrine Bridge. Decorating the ``Result`` class follows the same principle.

The middleware we've just created applies by default to all the connections.
If your application has several dbal connections, you can limit the middleware
scope to a subset of connections thanks to the ``AsMiddleware`` PHP attribute.
Let's limit our middleware to a connection named ``legacy``:

.. code-block:: php
<?php
namespace App\Middleware;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
#[AsMiddleware(connections: ['legacy'])]
class PreventRootConnectionMiddleware implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new PreventRootConnectionDriver($driver);
}
}
All the examples presented above assume ``autoconfigure`` is enabled.
If ``autoconfigure`` is disabled, the ``doctrine.middleware`` tag must be
added to the middleware. This tag supports a ``connections`` attribute to
limit the scope of the middleware.

.. note::

Middlewares have been introduced in version 3.2 of ``doctrine/dbal``
and at least the 2.6 version of ``doctrine/doctrine-bundle`` is needed
to integrate them in Symfony as shown above.

54 changes: 54 additions & 0 deletions Tests/DependencyInjection/DoctrineExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ public function testDbalOverrideDefaultConnection(): void
$this->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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down
3 changes: 1 addition & 2 deletions Tests/baseline-ignore
Original file line number Diff line number Diff line change
@@ -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\.%
%Using XML mapping driver with XSD validation disabled is deprecated%

0 comments on commit 2170e49

Please sign in to comment.