Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow userland middlewares #1472

Merged
merged 1 commit into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Attribute/AsMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Doctrine\Bundle\DoctrineBundle\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class AsMiddleware
{
/** @param string[] $connections */
public function __construct(
public array $connections = [],
) {
}
}
58 changes: 58 additions & 0 deletions DependencyInjection/Compiler/MiddlewaresPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;

use Doctrine\Bundle\DoctrineBundle\Middleware\ConnectionNameAwareInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use function array_keys;
use function in_array;
use function is_subclass_of;
use function sprintf;

final class MiddlewaresPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$middlewareAbstractDefs = [];
$middlewareConnections = [];
foreach ($container->findTaggedServiceIds('doctrine.middleware') as $id => $tags) {
$middlewareAbstractDefs[$id] = $container->getDefinition($id);
// When a def has doctrine.middleware tags with connection attributes equal to connection names
// registration of this middleware is limited to the connections with these names
foreach ($tags as $tag) {
if (! isset($tag['connection'])) {
continue;
}

$middlewareConnections[$id][] = $tag['connection'];
}
}

foreach (array_keys($container->getParameter('doctrine.connections')) as $name) {
$middlewareDefs = [];
foreach ($middlewareAbstractDefs as $id => $abstractDef) {
if (isset($middlewareConnections[$id]) && ! in_array($name, $middlewareConnections[$id], true)) {
continue;
}

$middlewareDefs[] = $childDef = $container->setDefinition(
sprintf('%s.%s', $id, $name),
new ChildDefinition($id)
);

if (! is_subclass_of($abstractDef->getClass(), ConnectionNameAwareInterface::class)) {
continue;
}

$childDef->addMethodCall('setConnectionName', [$name]);
l-vo marked this conversation as resolved.
Show resolved Hide resolved
}

$container
->getDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name))
->addMethodCall('setMiddlewares', [$middlewareDefs]);
}
}
}
51 changes: 38 additions & 13 deletions DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
use Doctrine\Bundle\DoctrineBundle\CacheWarmer\DoctrineMetadataCacheWarmer;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\ImportDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Dbal\ManagerRegistryAwareConnectionProvider;
Expand All @@ -13,8 +14,9 @@
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface;
use Doctrine\DBAL\Logging\LoggerChain;
use Doctrine\DBAL\Logging\Middleware;
use Doctrine\DBAL\Sharding\PoolingShardConnection;
use Doctrine\DBAL\Sharding\PoolingShardManager;
use Doctrine\DBAL\Tools\Console\Command\ImportCommand;
Expand Down Expand Up @@ -44,7 +46,6 @@
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
Expand All @@ -65,6 +66,8 @@
use function sprintf;
use function str_replace;

use const PHP_VERSION_ID;

/**
* DoctrineExtension is an extension for the Doctrine DBAL and ORM library.
*/
Expand Down Expand Up @@ -143,9 +146,33 @@ protected function dbalLoad(array $config, ContainerBuilder $container)
$container->setParameter('doctrine.connections', $connections);
$container->setParameter('doctrine.default_connection', $this->defaultConnection);

$connWithLogging = [];
foreach ($config['connections'] as $name => $connection) {
if ($connection['logging']) {
$connWithLogging[] = $name;
}

$this->loadDbalConnection($name, $connection, $container);
}

/** @psalm-suppress UndefinedClass */
$container->registerForAutoconfiguration(MiddlewareInterface::class)->addTag('doctrine.middleware');

if (PHP_VERSION_ID >= 80000 && method_exists(ContainerBuilder::class, 'registerAttributeForAutoconfiguration')) {
$container->registerAttributeForAutoconfiguration(AsMiddleware::class, static function (ChildDefinition $definition, AsMiddleware $attribute) {
if ($attribute->connections === []) {
$definition->addTag('doctrine.middleware');

return;
}

foreach ($attribute->connections as $connName) {
$definition->addTag('doctrine.middleware', ['connection' => $connName]);
}
});
}

$this->useMiddlewaresIfAvailable($container, $connWithLogging);
}

/**
Expand All @@ -160,7 +187,6 @@ protected function loadDbalConnection($name, array $connection, ContainerBuilder
$configuration = $container->setDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name), new ChildDefinition('doctrine.dbal.connection.configuration'));
$logger = null;
if ($connection['logging']) {
$this->useMiddlewaresIfAvailable($connection, $container, $name, $configuration);
$logger = new Reference('doctrine.dbal.logger');
}

Expand Down Expand Up @@ -1073,25 +1099,24 @@ private function createArrayAdapterCachePool(ContainerBuilder $container, string
return $id;
}

/** @param array<string, mixed> $connection */
protected function useMiddlewaresIfAvailable(array $connection, ContainerBuilder $container, string $name, Definition $configuration): void
/** @param string[] $connWithLogging */
private function useMiddlewaresIfAvailable(ContainerBuilder $container, array $connWithLogging): void
{
/** @psalm-suppress UndefinedClass */
if (! interface_exists(Middleware::class)) {
if (! class_exists(Middleware::class)) {
return;
}

$container
->getDefinition('doctrine.dbal.logger')
->replaceArgument(0, null);

$loggingMiddlewareDef = $container->setDefinition(
sprintf('doctrine.dbal.%s_connection.logging_middleware', $name),
new ChildDefinition('doctrine.dbal.logging_middleware')
);
$loggingMiddlewareDef->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE));
$loggingMiddlewareDef->addTag('monolog.logger', ['channel' => 'doctrine']);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('middlewares.xml');

$configuration->addMethodCall('setMiddlewares', [[$loggingMiddlewareDef]]);
$loggingMiddlewareAbstractDef = $container->getDefinition('doctrine.dbal.logging_middleware');
foreach ($connWithLogging as $connName) {
$loggingMiddlewareAbstractDef->addTag('doctrine.middleware', ['connection' => $connName]);
}
}
}
8 changes: 8 additions & 0 deletions DoctrineBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DbalSchemaFilterPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\MiddlewaresPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveProfilerControllerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\WellKnownSchemaFilterPass;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Proxy\Autoloader;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\DoctrineValidationPass;
Expand All @@ -26,6 +28,7 @@
use function assert;
use function class_exists;
use function clearstatcache;
use function interface_exists;
use function spl_autoload_unregister;

class DoctrineBundle extends Bundle
Expand Down Expand Up @@ -60,6 +63,11 @@ public function build(ContainerBuilder $container)
$container->addCompilerPass(new CacheSchemaSubscriberPass(), PassConfig::TYPE_BEFORE_REMOVING, -10);
$container->addCompilerPass(new RemoveProfilerControllerPass());

/** @psalm-suppress UndefinedClass */
if (interface_exists(Middleware::class)) {
$container->addCompilerPass(new MiddlewaresPass());
}

if (! class_exists(RegisterUidTypePass::class)) {
return;
}
Expand Down
8 changes: 8 additions & 0 deletions Middleware/ConnectionNameAwareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Doctrine\Bundle\DoctrineBundle\Middleware;

interface ConnectionNameAwareInterface
{
public function setConnectionName(string $name): void;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for this?

Copy link
Contributor Author

@l-vo l-vo Feb 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this PR with the replacement of DebugStack by middlewares in mind. In the symfony profiler, queries are grouped by connection. I faced a choice:

  • either I inject in the middleware a different service (kind of debug data holder) by connection
  • either the middleware is aware of the connection and set queries in an unique debug data holder tied with their connection

It's not a big deal to duplicate an abstract middleware for each connection with a tag, but duplicating too the debug data holder seemed to me more complicated; it's why I chose the second possibility.

This is how I plan to create the debug data holder:

class DebugDataHolder
{
    private array $data = [];

    public function addQuery(string $connexionName, Query $query): void
    {
        $this->data[$connexionName][] = [
            'sql' => $query->getSql(),
            'params' => $query->getParams(),
            'types' => $query->getTypes(),
            'executionMS' => static fn() => $query->getDuration(),  // stop() may not be called at this point
        ];
    }

    public function getData(string $connexionName): array
    {
        if (!isset($this->data[$connexionName])) {
            return [];
        }

        foreach ($this->data[$connexionName] as &$data) {
            if (is_callable($data['executionMS'])) {
                $data['executionMS'] = $data['executionMS']();
            }
        }

        return $this->data[$connexionName];
    }
}

3 changes: 0 additions & 3 deletions Resources/config/dbal.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@
<argument type="service" id="debug.stopwatch" on-invalid="null" />
</service>

<service id="doctrine.dbal.logging_middleware" class="Doctrine\DBAL\Logging\Middleware" abstract="true">
</service>

<service id="data_collector.doctrine" class="%doctrine.data_collector.class%" public="false">
<tag name="data_collector" template="@Doctrine/Collector/db.html.twig" id="db" priority="250" />
<argument type="service" id="doctrine" />
Expand Down
14 changes: 14 additions & 0 deletions Resources/config/middlewares.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="doctrine.dbal.logging_middleware" class="Doctrine\DBAL\Logging\Middleware" abstract="true">
<argument type="service" id="logger" on-invalid="null" />
<tag name="monolog.logger" channel="doctrine" />
<tag name="doctrine.middleware" />
</service>
</services>
</container>
Loading