Skip to content

Commit

Permalink
feature #30691 [Contracts][EventDispatcher] add EventDispatcherInterf…
Browse files Browse the repository at this point in the history
…ace to symfony/contracts and use it where possible (nicolas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[Contracts][EventDispatcher] add EventDispatcherInterface to symfony/contracts and use it where possible

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR adds a new `EventDispatcherInterface` in `Contracts`. This interface contains only one method: `dispatch($event, $eventName)`. That covers almost all use cases of the event dispatcher in components (some use add/removeListeners/Subscribers but they are a minority.)

While doing so, it allows dispatching any objects as events - not only instances of `Event`.

This allows decoupling e.g. `Messenger` from the `EventDispatcher` component.
Next steps could be about planning to remove the base `Event` class from some concrete event classes and/or moving `EventSubscriberInterface` to `symfony/contracts`. It would allow decoupling e.g. `Workflow` from the component - but that's too far away for now, let's think about it in 5.1.

Commits
-------

3c3db2f [Contracts][EventDispatcher] add EventDispatcherInterface to symfony/contracts and use it where possible
  • Loading branch information
fabpot committed Mar 27, 2019
2 parents 08faa53 + 3c3db2f commit c949f9a
Show file tree
Hide file tree
Showing 55 changed files with 196 additions and 92 deletions.
Expand Up @@ -49,6 +49,7 @@
<tag name="container.hot_path" />
</service>
<service id="Symfony\Component\EventDispatcher\EventDispatcherInterface" alias="event_dispatcher" />
<service id="Symfony\Contracts\EventDispatcher\EventDispatcherInterface" alias="event_dispatcher" />

<service id="http_kernel" class="Symfony\Component\HttpKernel\HttpKernel" public="true">
<argument type="service" id="event_dispatcher" />
Expand Down
Expand Up @@ -13,7 +13,7 @@

use Doctrine\Common\Annotations\Reader;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class AutowiredServices
{
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -20,7 +20,7 @@
"ext-xml": "*",
"symfony/cache": "~4.3",
"symfony/config": "~4.2",
"symfony/contracts": "^1.0.2",
"symfony/contracts": "^1.1",
"symfony/dependency-injection": "^4.3",
"symfony/http-foundation": "^4.3",
"symfony/http-kernel": "^4.3",
Expand Down
Expand Up @@ -30,6 +30,8 @@ class FirewallListener extends Firewall

public function __construct(FirewallMapInterface $map, EventDispatcherInterface $dispatcher, LogoutUrlGenerator $logoutUrlGenerator)
{
// the type-hint will be updated to the "EventDispatcherInterface" from symfony/contracts in 5.0

$this->map = $map;
$this->logoutUrlGenerator = $logoutUrlGenerator;

Expand Down
Expand Up @@ -17,7 +17,6 @@
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
Expand All @@ -34,6 +33,7 @@
use Symfony\Component\Security\Core\Role\SwitchUserRole;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class SecurityDataCollectorTest extends TestCase
{
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -91,6 +91,9 @@ public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN
$this->defaultCommand = 'list';
}

/**
* @final since Symfony 4.3, the type-hint will be updated to the interface from symfony/contracts in 5.0
*/
public function setDispatcher(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = LegacyEventDispatcherProxy::decorate($dispatcher);
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Console/composer.json
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": "^7.1.3",
"symfony/contracts": "^1.0",
"symfony/contracts": "^1.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8"
},
Expand Down
Expand Up @@ -226,16 +226,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
if ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} elseif (!$parameter->allowsNull()) {
if (\function_exists('xdebug_disable')) {
xdebug_disable();
}
try {
throw new AutowiringFailedException($this->currentId, $failureMessage);
} finally {
if (\function_exists('xdebug_enable')) {
xdebug_enable();
}
}
throw new AutowiringFailedException($this->currentId, $failureMessage);
}
}

Expand Down
Expand Up @@ -23,6 +23,10 @@ public function __construct(string $serviceId, $message = '', int $code = 0, \Ex
{
$this->serviceId = $serviceId;

if ($message instanceof \Closure && \function_exists('xdebug_is_enabled') && xdebug_is_enabled()) {
$message = $message();
}

if (!$message instanceof \Closure) {
parent::__construct($message, $code, $previous);

Expand Down
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\EventDispatcher\Debug;

use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -133,7 +134,7 @@ public function dispatch($event/*, string $eventName = null*/)

$eventName = 1 < \func_num_args() ? \func_get_arg(1) : null;

if ($event instanceof Event) {
if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event);
} else {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as first argument is deprecated since Symfony 4.3, pass it second and provide the event object first instead.', EventDispatcherInterface::class), E_USER_DEPRECATED);
Expand All @@ -146,13 +147,13 @@ public function dispatch($event/*, string $eventName = null*/)
}
}

if (null !== $this->logger && $event->isPropagationStopped()) {
if (null !== $this->logger && ($event instanceof Event || $event instanceof StoppableEventInterface) && $event->isPropagationStopped()) {
$this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
}

$this->preProcess($eventName);
try {
$this->preDispatch($eventName, $event);
$this->beforeDispatch($eventName, $event);
try {
$e = $this->stopwatch->start($eventName, 'section');
try {
Expand All @@ -163,7 +164,7 @@ public function dispatch($event/*, string $eventName = null*/)
}
}
} finally {
$this->postDispatch($eventName, $event);
$this->afterDispatch($eventName, $event);
}
} finally {
$this->postProcess($eventName);
Expand Down Expand Up @@ -262,18 +263,32 @@ public function __call($method, $arguments)
/**
* Called before dispatching the event.
*
* @param string $eventName The event name
* @param Event $event The event
* @param object $event
*/
protected function preDispatch($eventName, Event $event)
protected function beforeDispatch(string $eventName, $event)
{
$this->preDispatch($eventName, $event);
}

/**
* Called after dispatching the event.
*
* @param string $eventName The event name
* @param Event $event The event
* @param object $event
*/
protected function afterDispatch(string $eventName, $event)
{
$this->postDispatch($eventName, $event);
}

/**
* @deprecated since Symfony 4.3, will be removed in 5.0, use beforeDispatch instead
*/
protected function preDispatch($eventName, Event $event)
{
}

/**
* @deprecated since Symfony 4.3, will be removed in 5.0, use afterDispatch instead
*/
protected function postDispatch($eventName, Event $event)
{
Expand Down
Expand Up @@ -11,13 +11,16 @@

namespace Symfony\Component\EventDispatcher\Debug;

use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Caster\ClassStub;

/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.3: the "Event" type-hint on __invoke() will be replaced by "object" in 5.0
*/
class WrappedListener
{
Expand Down Expand Up @@ -120,7 +123,7 @@ public function __invoke(Event $event, $eventName, EventDispatcherInterface $dis
$e->stop();
}

if ($event->isPropagationStopped()) {
if (($event instanceof Event || $event instanceof StoppableEventInterface) && $event->isPropagationStopped()) {
$this->stoppedPropagation = true;
}
}
Expand Down
18 changes: 15 additions & 3 deletions src/Symfony/Component/EventDispatcher/EventDispatcher.php
Expand Up @@ -11,6 +11,8 @@

namespace Symfony\Component\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;

/**
* The EventDispatcherInterface is the central point of Symfony's event listener system.
*
Expand Down Expand Up @@ -48,7 +50,7 @@ public function dispatch($event/*, string $eventName = null*/)
{
$eventName = 1 < \func_num_args() ? \func_get_arg(1) : null;

if ($event instanceof Event) {
if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event);
} else {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as first argument is deprecated since Symfony 4.3, pass it second and provide the event object first instead.', EventDispatcherInterface::class), E_USER_DEPRECATED);
Expand Down Expand Up @@ -223,12 +225,22 @@ public function removeSubscriber(EventSubscriberInterface $subscriber)
*
* @param callable[] $listeners The event listeners
* @param string $eventName The name of the event to dispatch
* @param Event $event The event object to pass to the event handlers/listeners
* @param object $event The event object to pass to the event handlers/listeners
*/
protected function callListeners(iterable $listeners, string $eventName, $event)
{
$this->doDispatch($listeners, $eventName, $event);
}

/**
* @deprecated since Symfony 4.3, use callListeners() instead
*/
protected function doDispatch($listeners, $eventName, Event $event)
{
$stoppable = $event instanceof Event || $event instanceof StoppableEventInterface;

foreach ($listeners as $listener) {
if ($event->isPropagationStopped()) {
if ($stoppable && $event->isPropagationStopped()) {
break;
}
$listener($event, $eventName, $this);
Expand Down
15 changes: 3 additions & 12 deletions src/Symfony/Component/EventDispatcher/EventDispatcherInterface.php
Expand Up @@ -11,26 +11,17 @@

namespace Symfony\Component\EventDispatcher;

use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;

/**
* The EventDispatcherInterface is the central point of Symfony's event listener system.
* Listeners are registered on the manager and events are dispatched through the
* manager.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface EventDispatcherInterface
interface EventDispatcherInterface extends ContractsEventDispatcherInterface
{
/**
* Dispatches an event to all registered listeners.
*
* @param Event $event The event to pass to the event handlers/listeners
* @param string|null $eventName The name of the event to dispatch. If not supplied,
* the class of $event should be used instead.
*
* @return Event
*/
public function dispatch($event/*, string $eventName = null*/);

/**
* Adds an event listener that listens on the specified events.
*
Expand Down
Expand Up @@ -11,6 +11,9 @@

namespace Symfony\Component\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
* An helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch().
*
Expand Down Expand Up @@ -51,7 +54,7 @@ public function dispatch($event/*, string $eventName = null*/)
{
$eventName = 1 < \func_num_args() ? \func_get_arg(1) : null;

if ($event instanceof Event) {
if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event);
} else {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as first argument is deprecated since Symfony 4.3, pass it second and provide the event object first instead.', EventDispatcherInterface::class), E_USER_DEPRECATED);
Expand All @@ -65,9 +68,10 @@ public function dispatch($event/*, string $eventName = null*/)
}

$listeners = $this->getListeners($eventName);
$stoppable = $event instanceof Event || $event instanceof StoppableEventInterface;

foreach ($listeners as $listener) {
if ($event->isPropagationStopped()) {
if ($stoppable && $event->isPropagationStopped()) {
break;
}
$listener($event, $eventName, $this);
Expand Down
6 changes: 5 additions & 1 deletion src/Symfony/Component/EventDispatcher/composer.json
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": "^7.1.3",
"symfony/contracts": "^1.0"
"symfony/contracts": "^1.1"
},
"require-dev": {
"symfony/dependency-injection": "~3.4|~4.0",
Expand All @@ -29,6 +29,10 @@
"conflict": {
"symfony/dependency-injection": "<3.4"
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "1.1"
},
"suggest": {
"symfony/dependency-injection": "",
"symfony/http-kernel": ""
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Component/Form/Test/TypeTestCase.php
Expand Up @@ -11,7 +11,7 @@

namespace Symfony\Component\Form\Test;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;

Expand All @@ -25,15 +25,15 @@ abstract class TypeTestCase extends FormIntegrationTestCase
protected $builder;

/**
* @var EventDispatcher
* @var EventDispatcherInterface
*/
protected $dispatcher;

private function doSetUp()
{
parent::setUp();

$this->dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
$this->dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
$this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory);
}

Expand Down
Expand Up @@ -13,9 +13,9 @@

use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
Expand Down
Expand Up @@ -27,7 +27,7 @@ class TraceableEventDispatcher extends BaseTraceableEventDispatcher
/**
* {@inheritdoc}
*/
protected function preDispatch($eventName, Event $event)
protected function beforeDispatch(string $eventName, $event)
{
switch ($eventName) {
case KernelEvents::REQUEST:
Expand Down Expand Up @@ -58,7 +58,7 @@ protected function preDispatch($eventName, Event $event)
/**
* {@inheritdoc}
*/
protected function postDispatch($eventName, Event $event)
protected function afterDispatch(string $eventName, $event)
{
switch ($eventName) {
case KernelEvents::CONTROLLER_ARGUMENTS:
Expand Down
Expand Up @@ -54,6 +54,10 @@ public function logKernelException(GetResponseForExceptionEvent $event)
$this->logException($event->getException(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()));
}

/**
* @param string $eventName
* @param EventDispatcherInterface $eventDispatcher
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
if (null === $this->controller) {
Expand Down

0 comments on commit c949f9a

Please sign in to comment.