支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
我们的框架始终缺乏一个优秀框架的关键特性:扩展性。可扩展意味着开发者应该可以轻易打入(hook)框架的生命周期来调整请求被处理的方式。
我们讨论的是何种hook方式?例如,认证或缓存。为了灵活,这种“打入”必须是“即插即用”的(plug-and-play)。你注册到程序中的hooks是不能等同于另外一个依赖于你的特定需求的(hook)。很多软件有相似概念,比如Drupal或WordPress。在一些语言中,甚至有诸如Python的WSGI和Ruby的Rack等等一些标准。
由于在PHP中无有标准,我们将使用一个广为人知的设计模式,即Mediator(中介者),来允许任何各类的行为被附着到我们的框架之中。Symfony的EventDispatcher组件实现了这种模式的一个轻量化版本:
1 | $ composer require symfony/event-dispatcher |
它是如何工作的?dispatcher(派遣器)是事件派遣系统的中心对象(central object),它通知那些“针对某个事件”的监听器。换一种表达方式:你的代码派遣一个事件到dispatcher,dispatcher通知所有已经注册到该事件的监听,然后每个监听对这个事件去做它们希望的任何事。
作为例子,我们创建一个监听来透明地添加Google Analytics代码(谷歌统计代码)到所有响应中。
要让它工作,框架必须派遣一个事件,在响应实例被返回之前:
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 $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
事件就被派遣一次:
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\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;
}
} |
最后一步是在前端控制器中创建dispatcher,然后注册一个监听到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 | // 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');
});
// 官方文档在下面这行少了一个参数,即第四个argumentResolver,请大家注意
$framework = new Simplex\Framework($dispatcher, $matcher, $resolver);
$response = $framework->handle($request);
$response->send(); |
此处的监听只用作概念证明,你应该在body标签之前添加Google统计代码。
如你所见,addListener()
关联了一个指向了“命名事件”(response
)的有效PHP回调。事件名称必须和使用在dispatch()
方法中的那个一样。
在监听内部,我们仅在响应“没有被重定向”、请求的格式(format)是HTML并且响应的content-type也是HTML的时候(这些条件演示了在操作你代码中的请求和响应数据时是何其容易),添加了Google统计代码。
到目前为止一切还好,但让我们对同一事件添加另一个监听。说我们想要设置响应的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()));
}
}); |
当创建你的框架时,要考虑优先级(例如对内部监听器的某些数字进行保留),同时全面文档化。
通过转移谷歌统计代码到它自己的类,我们略微重构一下代码:
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');
}
} |
然后在另一个监听中做同样的事:
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()));
}
}
} |
我们的前端控制器应该是下面这样:
就算类中的代码已经被漂亮地“打了包”,还是有一点问题:(监听器的)优先级信息被写死在前端控制器中了,而没有在监听器内部进行。在每套程序中,你不得不记起要设置合适的优先级。更甚者,监听方法的名字也被暴露在这里,这意味着重构监听器时将会改变所有依赖此监听的程序。当然,有一个解决方案,就是使用订阅器而非监听:
一个订阅器(subscriber)知晓它感兴趣的所有事件,并通过getSubscriberEvents()
方法将此信息传至dispatcher(派遣器):
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 array('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()
{
return array('response' => array('onResponse', -255));
}
} |
一个单一的订阅器即可托管你想要“随需针对任意数量事件进行监听”的全部Listener。
为了让你的框架真正灵活,应毫不犹豫地添加更多事件;为了让它更震撼人心,应添加更多的监听。再一次,本系列文章非是要创建一个“通用框架”,而是一个“量体裁衣”满足你需要的框架。可以在你认为舒适的地方停下来,另行选择时间再从那里重新介入。
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。