使用 Symfony 的组件创建自己的 PHP 框架(第九部分:创建事件与处理事件)

使用 Symfony 的组件创建自己的 PHP 框架(第九部分:创建事件与处理事件)

Chris Yue No Comment
Posts

英文原文地址:https://symfony.com/doc/current/create_framework/event_dispatcher.html

我们的框架依然缺少作为好框架必备的一个特点:扩展性。拥有扩展性意味着,开发者可以很方便的通过拦截(hook)的方式,修改请求被处理的过程。

我们说的拦截是什么东西呢?验证或者缓存就是两个例子(译者注:其实请求的处理就像通过一层层的筛子,或过滤器。比如访问一个需要做身份验证的 URL,那么我们可以设计这么一个筛子,它将判断请求来源是否已登陆,如果没有登录请求将被拦截住,转向登录页面。如果一个 URL 设计为可以被缓存,一个请求过来以后,缓存筛子判断这个 URL 是否已经被缓存,如果是,这个筛子直接拦截此次请求不继续往下处理,而是直接把缓存的内容发送出去)。为了灵活性,拦截程序必须是即插即用型(plug and play)的。根据不同的需求,你所“注册”的拦截程序肯定有别于其他拦截程序。许多软件都有类似的概念,比如说 WordPress 或者 Drupal。在一些语言里,甚至会有相关的标准。比如 Ruby 的 Rack 和 Python 的 WSGI

因为在 PHP 里面没有相关的标准,我们将使用著名的设计模式“观察者模式”,来将各种拦截模块连接到我们的框架中。sf2 的事件调度(EventDispatcher)组件为此模式做了一个轻量级的实现。

composer require symfony/event-dispatcher

此模块如何工作呢?作为此组件的核心的调度器,将对每一个连接过它的监听器(listener)做出事件提醒。或者这么说:你的代码将在某个事件发生的时候调用调度器,而调度器将提醒每一个监听器刚刚发生了什么事件,每个监听器收到消息后,将对此事件做出自己的不同处理行为。

举一个例子,让我们透明的为每一个响应都添加 google 的网站访问分析代码 GA。

要达成此目的,我们得让框架在返回响应对象之前做一次事件分发:

<?php

// example.com/src/Simplex/Framework.php

namespace Simplex;

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

class Framework
{
    protected $matcher;
    protected $resolver;
    protected $dispatcher;

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

    public function handle(Request $request)
    {
        try {
            $request->attributes->add($this->matcher->match($request->getPathInfo()));

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

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

        // 分发响应事件
        $this->dispatcher->dispatch('response', new ResponseEvent($response, $request));

        return $response;
    }
}

框架每次处理请求的时候,都将分发一个类型为 ResponseEvent 的事件:

<?php

// example.com/src/Simplex/ResponseEvent.php

namespace Simplex;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\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;
    }
}

最后一步我们将创建分发器,并为此添加一个监听器:

<?php

// example.com/web/front.php

require_once __DIR__.'/../vendor/.composer/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');
});

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

$response->send();

上面代码只是为了说明怎么加代码,若要真的添加 GA 代码你得自己把真实的代码写上去(译者注:而且用这种方式添加 GA 在真正实践中也不是一个好方法,像 GA 这种 HTML 代码,直接放在布局文件中更好)

如你所见,addListener() 方法把将 response 事件和一个回调函数联系在了一起。事件的名字必须跟 dispatch() 方法里提到的事件名字一样。

在监听代码中,我们只为不跳转的响应添加 GA 代码,而且响应的类型必须是 html 类型(此代码演示了操作请求或者响应对象是件多么手到擒来的事情)(译者注:你肯定不想为类似生成图片的响应添加 GA 代码)

让我们在为同样的事件添加另外一个监听者,此监听者检查响应是否有 Content-length 头,没有便添加一个:

$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()));
    }
});

添加监听者时需要注意添加的顺序,否则你有可能会得到错误的 Content-length 值。监听者添加顺序很重要,但从默认设置来说,所有的监听者都是一样的优先级,值为 0。要想让某个监听者优先执行,可以将优先级设置为一个正数,也可以为低优先级的监听控件设置优先级为负数。比如说设置 content-length 的监听控件,我们可以设置它为 -255,表示最后执行。

$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);

当你创建框架的时候,请仔细思考如何设计优先级(比如给内部监听控件预留一些优先级数),以及为此做好文档

让我们将代码重构一下,把GA监听控件放在属于他自己的类里:

<?php

// 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');
    }
}

其他的监听也做同样处理:

<?php

// 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()));
        }
    }
}

如此以来我们的前端控制器代码可改成如下样子:

$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', array(
    new Simplex\ContentLengthListener(), 
    'onResponse'
), -255);
$dispatcher->addListener('response', array(new Simplex\GoogleListener(), 'onResponse'));

虽然相关代码已被漂亮的封装起来了,但依然还残留一个小问题:优先级相关的代码被写死在了前段控制器里,而不是监听器自己来控制。每一个应用程序里你都得自己记着设置合适的优先级。除此之外,监听器所绑定的事件名字也暴露在前段控制器里,这意味着如果我们要重构监听器时,所有相关的应用程序都得所相应的修改。当然,有一个办法可以解决这个问题,使用订阅的方式来代替监听的方式:

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

“订阅者”知道所有的事件,并且可通过getSubscribedEvents()方法给分发器传递信息。让我们看看新版本的GA监听器:

<?php

// example.com/src/Simplex/GoogleListener.php

namespace Simplex;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class GoogleListener implements EventSubscriberInterface
{
    // ...

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

以及新版Content-length监听器:

<?php

// example.com/src/Simplex/ContentLengthListener.php

namespace Simplex;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ContentLengthListener implements EventSubscriberInterface
{
    // ...

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

一个订阅者可以拥有多个监听者,以及处理多种事件

为了让框架更加的灵活,不要犹豫,给框架多添加点事件吧,当然为了让框架功能更给力,添加更多的监听器那也是必须的,直到你感觉合适了为止,然后我们可以更进一步改进我们的代码。

返回阅读第八部分 | 继续阅读第十部分

使用 Symfony 的组件创建自己的 PHP 框架(第九部分:创建事件与处理事件) by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人

发表评论

8 + 2 =