8000 Resolve event bubbling logic in a compiler pass · symfony/symfony@0d0e51f · GitHub
[go: up one dir, main page]

Skip to content

Commit 0d0e51f

Browse files
committed
Resolve event bubbling logic in a compiler pass
* This removes duplicate event dispatching logic on event bubbling, which probably improves performance. * It allows to still specify listener priorities while listening on a bubbled-up event (instead of a fix moment where the event bubbling occurs)
1 parent 269a7a8 commit 0d0e51f

File tree

5 files changed

+242
-52
lines changed

5 files changed

+242
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\LogicException;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
19+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
20+
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
21+
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
22+
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
23+
use Symfony\Component\Security\Http\Event\LogoutEvent;
24+
25+
/**
26+
* Makes sure all event listeners on the global dispatcher are also listening
27+
* to events on the firewall-specific dipatchers.
28+
*
29+
* This compiler pass must be run after RegisterListenersPass of the
30+
* EventDispatcher component.
31+
*
32+
* @author Wouter de Jong <wouter@wouterj.nl>
33+
*
34+
* @internal
35+
*/
36+
class RegisterGlobalSecurityEventListenersPass implements CompilerPassInterface
37+
{
38+
private static $eventBubblingEvents = [CheckPassportEvent::class, LoginFailureEvent::class, LoginSuccessEvent::class, LogoutEvent::class];
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function process(ContainerBuilder $container)
44+
{
45+
if (!$container->has('event_dispatcher') || !$container->has('security.authenticator.managers_locator')) {
46+
return;
47+
}
48+
49+
$firewallDispatchers = [];
50+
$authenticatorManagerLocator = $container->getDefinition('security.authenticator.managers_locator');
51+
foreach ($authenticatorManagerLocator->getArgument(0) as $firewallName => $managerArgument) {
52+
$firewallDispatchers[] = $container->findDefinition('security.event_dispatcher.'.$firewallName);
53+
}
54+
55+
$globalDispatcher = $container->findDefinition('event_dispatcher');
56+
foreach ($globalDispatcher->getMethodCalls() as $methodCall) {
57+
if ('addListener' !== $methodCall[0]) {
58+
continue;
59+
}
60+
61+
$methodCallArguments = $methodCall[1];
62+
if (!\in_array($methodCallArguments[0], self::$eventBubblingEvents, true)) {
63+
continue;
64+
}
65+
66+
foreach ($firewallDispatchers as $firewallDispatcher) {
67+
$firewallDispatcher->addMethodCall('addListener', $methodCallArguments);
68+
}
69+
}
70+
}
71+
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,6 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
348348
// Register Firewall-specific event dispatcher
349349
$firewallEventDispatcherId = 'security.event_dispatcher.'.$id;
350350
$container->register($firewallEventDispatcherId, EventDispatcher::class);
351-
$container->setDefinition($firewallEventDispatcherId.'.event_bubbling_listener', new ChildDefinition('security.event_dispatcher.event_bubbling_listener'))
352-
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
353351

354352
// Register listeners
355353
$listeners = [];

src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass;
1818
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
1919
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
20+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass;
2021
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
2122
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
2223
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
@@ -75,6 +76,8 @@ public function build(ContainerBuilder $container)
7576
$container->addCompilerPass(new RegisterCsrfFeaturesPass());
7677
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
7778
$container->addCompilerPass(new RegisterLdapLocatorPass());
79+
// must be registered after RegisterListenersPass (in the FrameworkBundle)
80+
$container->addCompilerPass(new RegisterGlobalSecurityEventListenersPass(), PassConfig::TYPE_BEFORE_REMOVING, -200);
7881

7982
$container->addCompilerPass(new AddEventAliasesPass([
8083
AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\SecurityBundle\SecurityBundle;
16+
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
17+
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
18+
use Symfony\Component\EventDispatcher\EventDispatcher;
19+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
21+
use Symfony\Component\DependencyInjection\ContainerBuilder;
22+
use Symfony\Component\DependencyInjection\Definition;
23+
use Symfony\Component\DependencyInjection\Reference;
24+
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
25+
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
26+
use Symfony\Component\Security\Http\Event\LogoutEvent;
27+
28+
class RegisterGlobalSecurtyEventListenersPassTest extends TestCase
29+
{
30+
private $container;
31+
32+
protected function setUp(): void
33+
{
34+
$this->container = new ContainerBuilder();
35+
$this->container->setParameter('kernel.debug', false);
36+
$this->container->register('request_stack', \stdClass::class);
37+
$this->container->register('event_dispatcher', EventDispatcher::class);
38+
39+
$this->container->registerExtension(new SecurityExtension());
40+
41+
$this->container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
42+
$this->container->getCompilerPassConfig()->setRemovingPasses([]);
43+
$this->container->getCompilerPassConfig()->setAfterRemovingPasses([]);
44+
45+
$securityBundle = new SecurityBundle();
46+
$securityBundle->build($this->container);
47+
}
48+
49+
public function testRegisterCustomListener()
50+
{
51+
$this->container->loadFromExtension('security', [
52+
'enable_authenticator_manager' => true,
53+
'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]],
54+
]);
55+
56+
$this->container->register('app.security_listener', \stdClass::class)
57+
->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class])
58+
->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]);
59+
60+
$this->container->compile();
61+
62+
$this->assertListeners([
63+
[LogoutEvent::class, ['app.security_listener', 'onLogout'], 0],
64+
[LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20],
65+
]);
66+
}
67+
68+
public function testRegisterCustomSubscriber()
69+
{
70+
$this->container->loadFromExtension('security', [
71+
'enable_authenticator_manager' => true,
72+
'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]],
73+
]);
74+
75+
$this->container->register(TestSubscriber::class)
76+
->addTag('kernel.event_subscriber');
77+
78+
$this->container->compile();
79+
80+
$this->assertListeners([
81+
[LogoutEvent::class, [TestSubscriber::class, 'onLogout'], -200],
82+
[CheckPassportEvent::class, [TestSubscriber::class, 'onCheckPassport'], 120],
83+
[LoginSuccessEvent::class, [TestSubscriber::class, 'onLoginSuccess'], 0],
84+
]);
85+
}
86+
87+
public function testMultipleFirewalls()
88+
{
89+
$this->container->loadFromExtension('security', [
90+
'enable_authenticator_manager' => true,
91+
'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true], 'api' => ['pattern' => '/api', 'http_basic' => true]],
92+
]);
93+
94+
$this->container->register('security.event_dispatcher.api', EventDispatcher::class)
95+
->addTag('security.event_dispatcher')
96+
->setPublic(true);
97+
98+
$this->container->register('app.security_listener', \stdClass::class)
99+
->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class])
100+
->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]);
101+
102+
$this->container->compile();
103+
104+
$this->assertListeners([
105+
[LogoutEvent::class, ['app.security_listener', 'onLogout'], 0],
106+
[LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20],
107+
], 'security.event_dispatcher.main');
108+
$this->assertListeners([
109+
[LogoutEvent::class, ['app.security_listener', 'onLogout'], 0],
110+
[LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20],
111+
], 'security.event_dispatcher.api');
112+
}
113+
114+
public function testListenerAlreadySpecific()
115+
{
116+
$this->container->loadFromExtension('security', [
117+
'enable_authenticator_manager' => true,
118+
'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]],
119+
]);
120+
121+
$this->container->register('security.event_dispatcher.api', EventDispatcher::class)
122+
->addTag('security.event_dispatcher')
123+
->setPublic(true);
124+
125+
$this->container->register('app.security_listener', \stdClass::class)
126+
->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class, 'dispatcher' => 'security.event_dispatcher.main'])
127+
->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]);
128+
129+
$this->container->compile();
130+
131+
$this->assertListeners([
132+
[LogoutEvent::class, ['app.security_listener', 'onLogout'], 0],
133+
[LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20],
134+
], 'security.event_dispatcher.main');
135+
}
136+
137+
private function assertListeners(array $expectedListeners, string $dispatcherId = 'security.event_dispatcher.main')
138+
{
139+
$actualListeners = [];
140+
foreach ($this->container->findDefinition($dispatcherId)->getMethodCalls() as $methodCall) {
141+
[$method, $arguments] = $methodCall;
142+
if ('addListener' !== $method) {
143+
continue;
144+
}
145+
146+
$arguments[1] = [(string) $arguments[1][0]->getValues()[0], $arguments[1][1]];
147+
$actualListeners[] = $arguments;
148+
}
149+
150+
$foundListeners = array_uintersect($expectedListeners, $actualListeners, function (array $a, array $b) {
151+
return $a === $b;
152+
});
153+
154+
$this->assertEquals($expectedListeners, $foundListeners);
155+
}
156+
}
157+
158+
class TestSubscriber implements EventSubscriberInterface
159+
{
160+
public static function getSubscribedEvents(): array
161+
{
162+
return [
163+
LogoutEvent::class => ['onLogout', -200],
164+
CheckPassportEvent::class => ['onCheckPassport', 120],
165+
LoginSuccessEvent::class => 'onLoginSuccess',
166+
];
167+
}
168+
}

0 commit comments

Comments
 (0)
0