跳到内容

EventDispatcher 组件

编辑此页

我们的框架仍然缺少任何优秀框架的一个主要特征:可扩展性。可扩展性意味着开发者应该能够钩入框架生命周期,以修改请求的处理方式。

我们正在讨论哪种钩子?例如,身份验证或缓存。为了灵活,钩子必须是即插即用的;为一个应用程序“注册”的钩子与下一个应用程序的钩子不同,这取决于你的具体需求。许多软件都有类似的概念,如 Drupal 或 WordPress。在某些语言中,甚至有一个标准,如 Python 中的 WSGI 或 Ruby 中的 Rack

由于 PHP 没有标准,我们将使用一个众所周知的设计模式,中介者,以允许任何类型的行为附加到我们的框架上;Symfony EventDispatcher 组件实现了这个模式的轻量级版本

1
$ composer require symfony/event-dispatcher

它是如何工作的?调度器,事件调度系统的中心对象,将事件通知给已注册的监听器。换句话说:你的代码将事件分发给调度器,调度器通知所有已注册的事件监听器,每个监听器对事件执行它想做的任何操作。

作为一个例子,让我们创建一个监听器,透明地将 Google Analytics 代码添加到所有响应中。

为了使其工作,框架必须在返回 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
// 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
{
    public function __construct(
        private EventDispatcher $dispatcher,
        private UrlMatcherInterface $matcher,
        private ControllerResolverInterface $controllerResolver,
        private ArgumentResolverInterface $argumentResolver,
    ) {
    }

    public function handle(Request $request): Response
    {
        $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;
    }
}

每次框架处理 Request 时,现在都会调度一个 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
// 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
{
    public function __construct(
        private Response $response,
        private Request $request,
    ) {
    }

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

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

最后一步是在前端控制器中创建调度器,并为 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
// 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): void {
    $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();

注意

监听器只是一个概念验证,你应该在 body 标签之前添加 Google Analytics 代码。

正如你所看到的,addListener() 将一个有效的 PHP 回调与一个命名的事件(response)关联起来;事件名称必须与 dispatch() 调用中使用的名称相同。

在监听器中,我们只在响应不是重定向、请求的格式是 HTML 并且响应内容类型是 HTML 时才添加 Google Analytics 代码(这些条件演示了从你的代码中操作 Request 和 Response 数据的简易性)。

目前为止一切都很好,但让我们在同一个事件上添加另一个监听器。假设我们想要设置 Response 的 Content-Length,如果它尚未设置

1
2
3
4
5
6
7
8
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void {
    $response = $event->getResponse();
    $headers = $response->headers;

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

取决于你是在之前的监听器注册之前还是之后添加这段代码,你将获得 Content-Length 标头的错误或正确值。有时,监听器的顺序很重要,但默认情况下,所有监听器都以相同的优先级 0 注册。要告诉调度器尽早运行监听器,请将优先级更改为正数;负数可以用于低优先级监听器。在这里,我们希望 Content-Length 监听器最后执行,因此将优先级更改为 -255

1
2
3
4
5
6
7
8
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void {
    $response = $event->getResponse();
    $headers = $response->headers;

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

提示

在创建框架时,考虑优先级(例如,为内部监听器保留一些数字)并彻底记录它们。

让我们通过将 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): void
    {
        $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');
    }
}

并对另一个监听器执行相同的操作

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): void
    {
        $response = $event->getResponse();
        $headers = $response->headers;

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

我们的前端控制器现在应该看起来像这样

1
2
3
$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', [new Simplex\ContentLengthListener(), 'onResponse'], -255);
$dispatcher->addListener('response', [new Simplex\GoogleListener(), 'onResponse']);

即使代码现在被很好地包装在类中,仍然存在一个小问题:优先级的知识被“硬编码”在前端控制器中,而不是在监听器本身中。对于每个应用程序,你都必须记住设置适当的优先级。此外,监听器方法名称也在此处公开,这意味着重构我们的监听器将意味着更改所有依赖这些监听器的应用程序。解决这个困境的方案是使用订阅器而不是监听器

1
2
3
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Simplex\ContentLengthListener());
$dispatcher->addSubscriber(new Simplex\GoogleListener());

订阅器知道它感兴趣的所有事件,并通过 getSubscribedEvents() 方法将此信息传递给调度器。看看新版本的 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(): array
    {
        return ['response' => 'onResponse'];
    }
}

这是新版本的 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(): array
    {
        return ['response' => ['onResponse', -255]];
    }
}

提示

单个订阅器可以在需要的任意多个事件上托管任意多个监听器。

为了使你的框架真正灵活,不要犹豫添加更多事件;为了使其开箱即用更棒,添加更多监听器。再次强调,本书不是关于创建通用框架,而是关于创建适合你需求的框架。在你认为合适的时候停止,并从那里进一步发展代码。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可协议获得许可。
目录
    版本