事件分发器组件
我们的框架仍然缺少好框架的一个重要特征:可扩展性。可扩展性意味着我们能够简单的在框架的生命周期中设置钩子,去修改请求控制的方式。
我们讨论的是钩子有哪些呢?举个例子来说,认证或者缓存。为了保证灵活性,钩子应该是即插即用的;取决的你的需求,你在不同应用注册的钩子也会不同。许多软件,例如Drupal或者Wordpress,都是相似的设计。在一些语言中,甚至有一个相关的标准,如果在Python中的WSGI或在Ruby中的Rack。
由于在PHP中没有这种标准,我们将要去使用一个著名的设计模式,中介者模式,它可以给我们的框架附加任何的行为; Symfony的事件分发器组件就是这个模式的一个轻量级版本:
$ composer require symfony/event-dispatcher
那么,事件分发器是如何工作的呢?分发器,是事件分发系统中核心的对象,通知监听器派遣给它的事件。换句话说,你的代码派遣一个事件给分发器,然后分发器通知所有该事件的已注册监听器,最后每个监听器对该事件作出响应。
举个例子,让我们来创建一个监听器,用来给每个响应添加Google分析的代码。
为了使该功能生效,框架必须在返回响应实例之前调度一个事件:
// 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 $e) {
$response = new Response('Not Found', 404);
} catch (\Exception $e) {
$response = new Response('An error occurred', 500);
}
// dispatch a response event
$this->dispatcher->dispatch('response', new ResponseEvent($response, $request));
return $response;
}
}
每次框架处理一个请求,就会调用一个ResponseEvent
事件:
// 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;
}
}
最后就是在front控制器中创建分发器,注册response
事件的监听器:
// 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();
上面的监听器只是一个大概的结构,你应该将具体Google分析的代码添加在body标签之前。
如你所见,addListener()
将一个合法的PHP回调函数与一个命名为response
的事件绑定;事件的名称必须与dispatch()
调用的名称相同。
在监听器中,如果响应不是一个重定向或者如果请求的格式是HTML,又或者响应的内容类型是HTML的时候,我们才去添加Google分析的代码(这些情况也证实了,在你的代码中,请求和响应的数据简易的可控性):
$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);
在你创建自己的框架的时候,你应该考虑优先级(例如为内部监听器储存一些优先级的数字)并完整的记录它们。
让我们再小小的重构下代码,将Google监听器移动到自己的类:
// 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');
}
}
然后,对另一个监听器做相同的处理:
// 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()));
}
}
}
我们的front控制器现在看上去就像下面这个样子了:
$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', array(new Simplex\ContentLengthListener(), 'onResponse'), -255);
$dispatcher->addListener('response', array(new Simplex\GoogleListener(), 'onResponse'));
监管现在代码很好的封装在了类中,但是仍然有一个小问题:优先级在front控制器是硬编码的,而不是在监听器中。 对于每一个应用,你都要记得去设置适当的优先级。而且,监听器方法的名称暴露了在这里,这意味着重构我们的监听器将要改变依赖于这些监听器的整个应用。当然,这里给出一个解决方案:使用订阅者而不是监听器:
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Simplex\ContentLengthListener());
$dispatcher->addSubscriber(new Simplex\GoogleListener());
一个订阅者了解它关注的所有事件,并且将这些信息通过getSubscribedEvents()
方法传递给分发器。来看看新版本的GoogleListener
:
// 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');
}
}
新版本的 ContentLengthListener
:
// 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));
}
}
单个订阅者可以掌管多个监听器,根据你所需要的事件。
为了使你的框架更灵活,不要犹豫,添加更多的事件吧;使它更完美,添加更多的监听器把。再次声明,这本书不是关于如何创建一个通用的框架,而是一个根据需要定制的框架。在任何你觉得合适的时候停下来,在此基础上继续改善代码。