PSR-14: A Major Event in PHP

in #php6 years ago (edited)

The PHP Framework Interoperability Group (PHP-FIG) has released a number of new specifications in the last year. The latest, PSR-14, covers Event Dispatching. Like many PSRs it's a fairly small spec, at the end of the day, but intended to be high-impact.

In this series of posts I want to cover what PSR-14 is and does (and what it isn't and doesn't), and how to best leverage it in your projects as it gets deployed more widely.

The goal

Event dispatching has been around for a long time, in various forms, in many languages. If you've used Symfony's EventDispatcher, Laravel's Events system, Drupal hooks, Zend Framework's Event Manager, the League\Event package, or anything along those lines then that's what we're talking about.

More generically, all of these systems are a form of "mediated observer". One piece of code emits a message of some kind (an "Event"), and a mediator passes that message to other, decoupled pieces of code (usually called "Listeners"). Sometimes it's mono-directional, other times those other pieces of code can pass data back to the caller in some fashion. They are all, of course, different and incompatible.

That creates a problem for stand-alone libraries that want to be able to connect to multiple frameworks and applications. Many libraries make themselves extensible by emitting events in some form or another for other code to tie into; however, that mediating layer is essentially proprietary. A library that injects the Symfony EventDispatcher is then coupled to Symfony; using it anywhere else requires installing EventDispatcher and bridging to whatever that framework's event system is. A library that calls Drupal's module_invoke_all() hook system is then coupled to Drupal. And so on.

The goal of PSR-14, then, is to abstract that dependency. It allows libraries to expose themselves to extension via a thin generic layer, which then makes it very simple to drop them into Symfony, Zend Framework, Laravel, TYPO3, eZ Platform, Slim, or any other environment without any extra overhead. As long as that environment has some PSR-14 compatible implementation, it will work.

The spec

As mentioned, the spec itself is fairly thin. It's really just three single-method interfaces, plus some meta-description about how to use them. That's a good thing. Let's have a look at them (comments removed to save space):

namespace Psr\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch(object $event);
}

interface ListenerProviderInterface
{
    public function getListenersForEvent(object $event) : iterable;
}

interface StoppableEventInterface
{
    public function isPropagationStopped() : bool;
}

The first two are the core of the specification; StoppableEventInterface is an extension we'll get to later.

The Dispatcher should be fairly familiar to most; it's just an object with a method you pass an Event to, that is, the mediator we mentioned before. The Event itself, though, is not defined; the Event can be any PHP object. More on that later.

Most existing implementations today have a single object (or set of functions) that acts as a mediator/dispatcher and a place to register the code that receives each Event (Listeners). For PSR-14, we deliberately split those two responsibilities into separate objects. The Dispatcher gets the list of Listeners from a Provider object, which it composes.

Where does the Provider get its Listeners? From anywhere it wants! There are a zillion-and-one ways to associate a Listener with an Event, all of them completely valid, and all of them incompatible. We decided early on that standardizing on One True Way(tm) of registering Listeners would be far too limiting. However, by standardizing how to get Listeners into the Dispatcher we can still offer a great deal of flexibility without overly restricting implementers' ability to do all kinds of weird things.

Also not specified in code is what Listeners look like. Listeners can be any PHP callable: A function, an anonymous function, a method of an object, whatever. Because a callable can do anything, that means it's also trivially easy and totally legitimate to have a Listener be, say, an anonymous function that lazy-loads a service out of a dependency injection container and calls a method on it, which is the code that was actually registered.

In short, the Dispatcher provides a simple, lightweight API for library authors. Listener Providers offer a robust and flexible API for framework integrators. And the Dispatcher/Provider relationship links the two together.

A trivial example

The most basic outline of how all the parts fit together, then, would look something like this:

class Dispatcher implements EventDispatcherInterface
{

   public function __construct(ListenerProviderInterface $provider)
   {
       $this->provider = $provider;
   }

   public function dispatch(object $event)
   {
       foreach ($this->provider->getListenersForEvent($event) as $listener) {
           $listener($event);
       }
       return $event;
   }
}

$dispatcher = new Dispatcher($provider);

$event = new SomethingHappened();
$dispatcher->dispatch($event);

That little bit of code, though, opens up a great deal of power and flexibility. In the rest of this series I will be fleshing it out further, going in depth on some of the design decisions, and a sampling of the many, many ways that lightweight Events can be used.

Code

PSR-14 already has coming support from major frameworks and applications:

  • Zend Framework's Matthew Weier O'Phinney has already committed to supporting it in zend-eventmanager 4.0 (coming soon).
  • Symfony just announced changes to the EventDispatcher component to bring it in line with PSR-14, which paves the way to support PSR-14 directly in 5.0/5.1.
  • The Yii framework has stated their intent to adopt PSR-14 in 3.0.
  • Benni Mack of the TYPO3 CMS has stated that TYPO3 will migrate all its existing hook+signal/slot concepts in favor of PSR-14 in the next release.

There's also three fully functional stand-alone implementations available that you can use today in any application:

All are just a composer require away. I'll be discussing them in more detail later in the series.

Credits

I want to give a shout-out to the whole PSR-14 Working Group:

This is my third stint as PSR Editor (fourth if you count PSR-8, the most important PSR ever), and despite being the largest group I've worked with it was also overall one of the most pleasant collaborative experiences I've ever had. the process was highly collaborative and deliberative throughout, which is as it should be. I am really happy with how it turned out in the end; may all our future endeavors in collaborative architecture be as productive.

PSR-14: The series