The EventDispatcher Component

Our framework is still missing a major characteristic of any good framework: extensibility. Being extensible means that the developer should be able to hook into the framework life cycle to modify the way the request is handled.

私たちのフレームワークには、優れたフレームワークの主要な特徴である拡張性がまだ欠けています。拡張可能であるということは、開発者がフレームワークのライフサイクルにフックして、リクエストの処理方法を変更できる必要があることを意味します。

What kind of hooks are we talking about? Authentication or caching for instance. To be flexible, hooks must be plug-and-play; the ones you "register" for an application are different from the next one depending on your specific needs. Many software have a similar concept like Drupal or WordPress. In some languages, there is even a standard like WSGI in Python or Rack in Ruby.

どのような種類のフックについて話しているのですか?たとえば、認証またはキャッシュ。柔軟性を持たせるには、フックをプラグ アンド プレイにする必要があります。アプリケーションに「登録」するものは、特定のニーズに応じて次のものとは異なります。 Drupal や WordPress のように、多くのソフトウェアには同様の概念があります。一部の言語では、Python の WSGI や Ruby の Rack のような標準さえあります。

As there is no standard for PHP, we are going to use a well-known design pattern, the Mediator, to allow any kind of behaviors to be attached to our framework; the Symfony EventDispatcher Component implements a lightweight version of this pattern:

PHP には標準がないため、よく知られている設計パターンであるメディエーターを使用して、あらゆる種類の動作をフレームワークに追加できるようにします。 Symfony EventDispatcher コンポーネントは、このパターンの軽量バージョンを実装します:
1
$ composer require symfony/event-dispatcher

How does it work? The dispatcher, the central object of the event dispatcher system, notifies listeners of an event dispatched to it. Put another way: your code dispatches an event to the dispatcher, the dispatcher notifies all registered listeners for the event, and each listener does whatever it wants with the event.

それはどのように機能しますか?イベントディスパッチャーシステムの中心的なオブジェクトであるディスパッチャーは、ディスパッチされたイベントをリスナーに通知します。別の言い方をすれば、コードがイベントをディスパッチャーにディスパッチし、ディスパッチャーがすべての登録済みリスナーにイベントを通知し、各リスナーがそのイベントに対して必要なことを何でも行うということです。

As an example, let's create a listener that transparently adds the Google Analytics code to all responses.

例として、GoogleAnalytics コードをすべての応答に透過的に追加するリスナーを作成してみましょう。

To make it work, the framework must dispatch an event just before returning the Response instance:

これを機能させるには、フレームワークが Response インスタンスを返す直前にイベントをディスパッチする必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;

class Framework
{
    private $dispatcher;
    private $matcher;
    private $controllerResolver;
    private $argumentResolver;

    public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $controllerResolver, ArgumentResolverInterface $argumentResolver)
    {
        $this->dispatcher = $dispatcher;
        $this->matcher = $matcher;
        $this->controllerResolver = $controllerResolver;
        $this->argumentResolver = $argumentResolver;
    }

    public function handle(Request $request)
    {
        $this->matcher->getContext()->fromRequest($request);

        try {
            $request->attributes->add($this->matcher->match($request->getPathInfo()));

            $controller = $this->controllerResolver->getController($request);
            $arguments = $this->argumentResolver->getArguments($request, $controller);

            $response = call_user_func_array($controller, $arguments);
        } catch (ResourceNotFoundException $exception) {
            $response = new Response('Not Found', 404);
        } catch (\Exception $exception) {
            $response = new Response('An error occurred', 500);
        }

        // dispatch a response event
        $this->dispatcher->dispatch(new ResponseEvent($response, $request), 'response');

        return $response;
    }
}

Each time the framework handles a Request, a ResponseEvent event is now dispatched:

フレームワークがリクエストを処理するたびに、ResponseEvent イベントが送出されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// example.com/src/Simplex/ResponseEvent.php
namespace Simplex;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\Event;

class ResponseEvent extends Event
{
    private $request;
    private $response;

    public function __construct(Response $response, Request $request)
    {
        $this->response = $response;
        $this->request = $request;
    }

    public function getResponse()
    {
        return $this->response;
    }

    public function getRequest()
    {
        return $this->request;
    }
}

The last step is the creation of the dispatcher in the front controller and the registration of a listener for the response event:

最後のステップは、フロント コントローラーでのディスパッチャーの作成と、応答イベントのリスナーの登録です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

// ...

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
    $response = $event->getResponse();

    if ($response->isRedirection()
        || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
        || 'html' !== $event->getRequest()->getRequestFormat()
    ) {
        return;
    }

    $response->setContent($response->getContent().'GA CODE');
});

$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

$framework = new Simplex\Framework($dispatcher, $matcher, $controllerResolver, $argumentResolver);
$response = $framework->handle($request);

$response->send();

Note

ノート

The listener is just a proof of concept and you should add the Google Analytics code just before the body tag.

リスナーは単なる概念実証であり、body タグの直前に GoogleAnalytics コードを追加する必要があります。

As you can see, addListener() associates a valid PHP callback to a named event (response); the event name must be the same as the one used in the dispatch() call.

ご覧のとおり、addListener() は有効な PHP コールバックを namedevent (応答) に関連付けます。イベント名は、dispatch() 呼び出しで使用されたものと同じでなければなりません。

In the listener, we add the Google Analytics code only if the response is not a redirection, if the requested format is HTML and if the response content type is HTML (these conditions demonstrate the ease of manipulating the Request and Response data from your code).

リスナーでは、応答がリダイレクトではない場合、要求された形式が HTML の場合、および応答のコンテンツ タイプが HTML の場合にのみ、Google アナリティクス コードを追加します (これらの条件は、コードからの要求および応答データの操作の容易さを示しています)。

So far so good, but let's add another listener on the same event. Let's say that we want to set the Content-Length of the Response if it is not already set:

ここまでは順調ですが、同じイベントに別のリスナーを追加してみましょう。まだ設定されていない場合は、応答の Content-Length を設定したいとしましょう。
1
2
3
4
5
6
7
8
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
    $response = $event->getResponse();
    $headers = $response->headers;

    if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
        $headers->set('Content-Length', strlen($response->getContent()));
    }
});

Depending on whether you have added this piece of code before the previous listener registration or after it, you will have the wrong or the right value for the Content-Length header. Sometimes, the order of the listeners matter but by default, all listeners are registered with the same priority, 0. To tell the dispatcher to run a listener early, change the priority to a positive number; negative numbers can be used for low priority listeners. Here, we want the Content-Length listener to be executed last, so change the priority to -255:

このコードを以前のリスナー登録の前に追加したか、その後に追加したかによって、Content-Length ヘッダーの値が間違っているか正しいかが決まります。リスナーの順序が重要な場合もありますが、デフォルトでは、すべてのリスナーが同じ優先度 0 で登録されます。リスナーを早期に実行するようにディスパッチャに指示するには、優先度を正の数に変更します。優先度の低いリスナーには負の数を使用できます。ここでは、Content-Length リスナーを最後に実行したいので、優先度を -255 に変更します。
1
2
3
4
5
6
7
8
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
    $response = $event->getResponse();
    $headers = $response->headers;

    if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
        $headers->set('Content-Length', strlen($response->getContent()));
    }
}, -255);

Tip

ヒント

When creating your framework, think about priorities (reserve some numbers for internal listeners for instance) and document them thoroughly.

フレームワークを作成するときは、優先順位について考え (たとえば、内部リスナー用にいくつかの番号を予約します)、それらを完全に文書化します。

Let's refactor the code a bit by moving the Google listener to its own class:

Google リスナーを独自のクラスに移動して、コードを少しリファクタリングしましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// example.com/src/Simplex/GoogleListener.php
namespace Simplex;

class GoogleListener
{
    public function onResponse(ResponseEvent $event)
    {
        $response = $event->getResponse();

        if ($response->isRedirection()
            || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
            || 'html' !== $event->getRequest()->getRequestFormat()
        ) {
            return;
        }

        $response->setContent($response->getContent().'GA CODE');
    }
}

And do the same with the other listener:

他のリスナーでも同じことを行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// example.com/src/Simplex/ContentLengthListener.php
namespace Simplex;

class ContentLengthListener
{
    public function onResponse(ResponseEvent $event)
    {
        $response = $event->getResponse();
        $headers = $response->headers;

        if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
            $headers->set('Content-Length', strlen($response->getContent()));
        }
    }
}

Our front controller should now look like the following:

フロントコントローラーは次のようになります。
1
2
3
$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', [new Simplex\ContentLengthListener(), 'onResponse'], -255);
$dispatcher->addListener('response', [new Simplex\GoogleListener(), 'onResponse']);

Even if the code is now nicely wrapped in classes, there is still a slight issue: the knowledge of the priorities is "hardcoded" in the front controller, instead of being in the listeners themselves. For each application, you have to remember to set the appropriate priorities. Moreover, the listener method names are also exposed here, which means that refactoring our listeners would mean changing all the applications that rely on those listeners. The solution to this dilemma is to use subscribers instead of listeners:

コードがクラスに適切にラップされたとしても、まだわずかな問題があります。優先順位の知識は、リスナー自体ではなく、フロント コントローラーに「ハードコーディング」されています。アプリケーションごとに、適切な優先順位を設定することを忘れないでください。さらに、リスナーのメソッド名もここで公開されます。つまり、リスナーをリファクタリングすると、それらのリスナーに依存するすべてのアプリケーションを変更することになります。このジレンマの解決策は、リスナーの代わりにサブスクライバーを使用することです。
1
2
3
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Simplex\ContentLengthListener());
$dispatcher->addSubscriber(new Simplex\GoogleListener());

A subscriber knows about all the events it is interested in and pass this information to the dispatcher via the getSubscribedEvents() method. Have a look at the new version of the GoogleListener:

サブスクライバは、関心のあるすべてのイベントを認識しており、この情報を getSubscribeEvents() メソッドを介してディスパッチャに渡します。 GoogleListener の新しいバージョンを見てみましょう:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example.com/src/Simplex/GoogleListener.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class GoogleListener implements EventSubscriberInterface
{
    // ...

    public static function getSubscribedEvents()
    {
        return ['response' => 'onResponse'];
    }
}

And here is the new version of ContentLengthListener:

ContentLengthListener の新しいバージョンは次のとおりです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example.com/src/Simplex/ContentLengthListener.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ContentLengthListener implements EventSubscriberInterface
{
    // ...

    public static function getSubscribedEvents()
    {
        return ['response' => ['onResponse', -255]];
    }
}

Tip

ヒント

A single subscriber can host as many listeners as you want on as many events as needed.

1 つのサブスクライバーは、必要な数のイベントで必要な数のリスナーをホストできます。

To make your framework truly flexible, don't hesitate to add more events; and to make it more awesome out of the box, add more listeners. Again, this book is not about creating a generic framework, but one that is tailored to your needs. Stop whenever you see fit, and further evolve the code from there.

フレームワークを真に柔軟にするために、ためらわずにイベントを追加してください。すぐに使用できるようにするには、リスナーを追加します。繰り返しますが、この本は一般的なフレームワークを作成するためのものではなく、ニーズに合わせて調整されたものです。必要に応じていつでも停止し、そこからコードをさらに進化させます。