feature #33793 [EventDispatcher] A compiler pass for aliased userland events (derrabus)

This PR was merged into the 4.4 branch.

Discussion
----------

[EventDispatcher] A compiler pass for aliased userland events

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | N/A
| License       | MIT
| Doc PR        | TODO

Since 4.3, the EventDispatcher component allows to register events via the FQCN of the class name instead of a dedicated event name.

Earlier this year I have worked with a team that used the event dispatcher for own custom events. When 4.3 was released, the team decided to use the new mechanism for new events. For the sake of consistency, we also wanted to migrate existing event subscribers to FQCN events.

While FrameworkBundle implements a nice aliasing mechanism for its own events, we couldn't find an obvious way to make use of FQCN event aliases for our own events. The best way we could find is registering a compiler pass that would extend an internal parameter that stores all event aliases. But that made us feel like we're fiddling with an implementation detail of the framework.

This PR aims to provide a standard way for applications and third-party bundles to register their own event aliases.

```php
$container->addCompilerPass(new EventAliasesPass([
    MyCustomEvent::class => 'my_custom_event',
]));
```

Furthermore, it adds tests for class aliasing to the component's test suite. Additionally, the newly introduced pass is dogfooded by the SecurityBundle, so FrameworkBundle doesn't need to know about events fired by the security components.

Commits
-------

34efe40371 [EventDispatcher] A compiler pass for aliased userland events.
This commit is contained in:
Nicolas Grekas 2019-10-04 13:06:19 +02:00
commit 8f92594576
13 changed files with 244 additions and 5 deletions

View File

@ -23,10 +23,6 @@
<parameter key="Symfony\Component\HttpKernel\Event\ViewEvent">kernel.view</parameter>
<parameter key="Symfony\Component\HttpKernel\Event\ExceptionEvent">kernel.exception</parameter>
<parameter key="Symfony\Component\HttpKernel\Event\TerminateEvent">kernel.terminate</parameter>
<parameter key="Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent">security.authentication.success</parameter>
<parameter key="Symfony\Component\Security\Core\Event\AuthenticationFailureEvent">security.authentication.failure</parameter>
<parameter key="Symfony\Component\Security\Http\Event\InteractiveLoginEvent">security.interactive_login</parameter>
<parameter key="Symfony\Component\Security\Http\Event\SwitchUserEvent">security.switch_user</parameter>
<parameter key="Symfony\Component\Workflow\Event\GuardEvent">workflow.guard</parameter>
<parameter key="Symfony\Component\Workflow\Event\LeaveEvent">workflow.leave</parameter>
<parameter key="Symfony\Component\Workflow\Event\TransitionEvent">workflow.transition</parameter>

View File

@ -78,6 +78,7 @@
"symfony/messenger": "<4.3",
"symfony/mime": "<4.4",
"symfony/property-info": "<3.4",
"symfony/security-bundle": "<4.4",
"symfony/serializer": "<4.2",
"symfony/stopwatch": "<3.4",
"symfony/translation": "<4.4",

View File

@ -33,7 +33,14 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMe
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Bundle.
@ -68,5 +75,12 @@ class SecurityBundle extends Bundle
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
$container->addCompilerPass(new AddEventAliasesPass([
AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS,
AuthenticationFailureEvent::class => AuthenticationEvents::AUTHENTICATION_FAILURE,
InteractiveLoginEvent::class => SecurityEvents::INTERACTIVE_LOGIN,
SwitchUserEvent::class => SecurityEvents::SWITCH_USER,
]));
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\DependencyInjection;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventSubscriber\TestSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
final class EventExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$container->register('test_subscriber', TestSubscriber::class)
->setPublic(true)
->addTag('kernel.event_subscriber');
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class EventBundle extends Bundle
{
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
final class TestSubscriber implements EventSubscriberInterface
{
public $calledMethods = [];
public static function getSubscribedEvents(): array
{
return [
AuthenticationSuccessEvent::class => 'onAuthenticationSuccess',
AuthenticationFailureEvent::class => 'onAuthenticationFailure',
InteractiveLoginEvent::class => 'onInteractiveLogin',
SwitchUserEvent::class => 'onSwitchUser',
];
}
public function __call(string $name, array $arguments)
{
$this->calledMethods[$name] = ($this->calledMethods[$name] ?? 0) + 1;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;
final class EventAliasTest extends AbstractWebTestCase
{
public function testAliasedEvents(): void
{
$client = $this->createClient(['test_case' => 'AliasedEvents', 'root_config' => 'config.yml']);
$container = $client->getContainer();
$dispatcher = $container->get('event_dispatcher');
$dispatcher->dispatch(new AuthenticationSuccessEvent($this->createMock(TokenInterface::class)), AuthenticationEvents::AUTHENTICATION_SUCCESS);
$dispatcher->dispatch(new AuthenticationFailureEvent($this->createMock(TokenInterface::class), new AuthenticationException()), AuthenticationEvents::AUTHENTICATION_FAILURE);
$dispatcher->dispatch(new InteractiveLoginEvent($this->createMock(Request::class), $this->createMock(TokenInterface::class)), SecurityEvents::INTERACTIVE_LOGIN);
$dispatcher->dispatch(new SwitchUserEvent($this->createMock(Request::class), $this->createMock(UserInterface::class), $this->createMock(TokenInterface::class)), SecurityEvents::SWITCH_USER);
$this->assertEquals(
[
'onAuthenticationSuccess' => 1,
'onAuthenticationFailure' => 1,
'onInteractiveLogin' => 1,
'onSwitchUser' => 1,
],
$container->get('test_subscriber')->calledMethods
);
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new EventBundle(),
];

View File

@ -0,0 +1,2 @@
imports:
- { resource: ./../config/framework.yml }

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.4.0
-----
* `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`.
4.3.0
-----

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* This pass allows bundles to extend the list of event aliases.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
class AddEventAliasesPass implements CompilerPassInterface
{
private $eventAliases;
private $eventAliasesParameter;
public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases')
{
$this->eventAliases = $eventAliases;
$this->eventAliasesParameter = $eventAliasesParameter;
}
public function process(ContainerBuilder $container): void
{
$eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : [];
$container->setParameter(
$this->eventAliasesParameter,
array_merge($eventAliases, $this->eventAliases)
);
}
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -67,6 +68,9 @@ class RegisterListenersPassTest extends TestCase
$builder->register('my_event_subscriber', AliasedSubscriber::class)
->addTag('kernel.event_subscriber');
$eventAliasPass = new AddEventAliasesPass([CustomEvent::class => 'custom_event']);
$eventAliasPass->process($builder);
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($builder);
@ -79,6 +83,14 @@ class RegisterListenersPassTest extends TestCase
0,
],
],
[
'addListener',
[
'custom_event',
[new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onCustomEvent'],
0,
],
],
];
$this->assertEquals($expectedCalls, $builder->getDefinition('event_dispatcher')->getMethodCalls());
}
@ -202,8 +214,12 @@ class RegisterListenersPassTest extends TestCase
$container = new ContainerBuilder();
$container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']);
$container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => AliasedEvent::class, 'method' => 'onEvent']);
$container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => CustomEvent::class, 'method' => 'onEvent']);
$container->register('event_dispatcher');
$eventAliasPass = new AddEventAliasesPass([CustomEvent::class => 'custom_event']);
$eventAliasPass->process($container);
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($container);
@ -217,6 +233,14 @@ class RegisterListenersPassTest extends TestCase
0,
],
],
[
'addListener',
[
'custom_event',
[new ServiceClosureArgument(new Reference('bar')), 'onEvent'],
0,
],
],
];
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
}
@ -249,6 +273,7 @@ final class AliasedSubscriber implements EventSubscriberInterface
{
return [
AliasedEvent::class => 'onAliasedEvent',
CustomEvent::class => 'onCustomEvent',
];
}
}
@ -256,3 +281,7 @@ final class AliasedSubscriber implements EventSubscriberInterface
final class AliasedEvent
{
}
final class CustomEvent
{
}

View File

@ -19,7 +19,7 @@
"php": "^7.1.3",
"symfony/error-handler": "^4.4|^5.0",
"symfony/error-renderer": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.3",
"symfony/event-dispatcher": "^4.4",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php73": "^1.9",