使用 Symfony 组件创建自己的 PHP 框架(第十一部分:HttpKernel 组件以及使用事件处理异常)

使用 Symfony 组件创建自己的 PHP 框架(第十一部分:HttpKernel 组件以及使用事件处理异常)

Chris Yue 7 comments
Posts

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

如果你正在使用我们的框架,你或许想让框架支持自定义错误页面。目前,我们可以处理 404 和 500 错误,不过此功能都是硬编码在框架里面的。但让错误信息变得可自定义也非常容易:分发一个新事件然后监听它。要做到这点也就意味着监听器(listener)需要调用一个控制器。

但要是这个控制器也抛异常,那不是会发生无限循环调用?应该有更方便的方法是吧。

下面看看 HttpKernel 类。HttpKernel 类是对 HttpKernelInterface 接口的一个可被广泛使用的,灵活的,可扩展的实现。我们就是用它而不用反反复复的发明轮子(译者注:又来了作者真讨厌发明轮子)。

这个类跟我们目前写的类非常的类似:它在一个请求进来的时候,在某些策略点(strategic point)分发事件,并使用控制器分析器来选择某一个控制器去处理请求。它会非常的细心处理各种问题,并且在有问题发生的时候,会有非常棒的回馈。

下面是我们的新框架代码:

<?php

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

namespace Simplex;

use Symfony\Component\HttpKernel\HttpKernel;

class Framework extends HttpKernel
{
}

以及新版控制器:

<?php

// example.com/web/front.php

require_once __DIR__.'/../vendor/.composer/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$context = new Routing\RequestContext();
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$resolver = new HttpKernel\Controller\ControllerResolver();

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));

$framework = new Simplex\Framework($dispatcher, $resolver);

$response = $framework->handle($request);
$response->send();

RouterListener 是一个跟我们之前实现的一样逻辑的类:匹配请求的路由,并且根据路由参数为请求生成更多的属性。

现在我们的代码既精简,功能又强大,比以前更加的给力了。比如说现在可以利用 ExceptionListener 监听器来让你的错误处理变得能够配置。

$errorHandler = function (\Symfony\Component\Debug\Exception\FlattenException $exception) {
    $msg = 'Something went wrong! ('.$exception->getMessage().')';

    return new Response($msg, $exception->getStatusCode());
});

$dispatcher->addSubscriber(new HttpKernel\EventListener\ExceptionListener($errorHandler));

ExceptionListener 使用 FlattenException 来代替抛出 Exception,让你更加容易地操作和显示异常。它能使用任何有效的控制器作为异常的处理器,所以你可以建立一个错误控制器(ErrorController 类)来代替闭包

$listener = new HttpKernel\EventListener\ExceptionListener(
    'Calendar\\Controller\\ErrorController::exceptionAction');
$dispatcher->addSubscriber($listener);

错误控制器代码如下:

<?php

// example.com/src/Calendar/Controller/ErrorController.php

namespace Calendar\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Exception\FlattenException;

class ErrorController
{
    public function exceptionAction(FlattenException $exception)
    {
        $msg = 'Something went wrong! ('.$exception->getMessage().')';

        return new Response($msg, $exception->getStatusCode());
    }
}

怎样?轻易得得到一个简洁而又可自定义的错误管理功能!如果你的控制器抛出一个异常,HttpKernel 会很好去处理它。

在第二章,我们曾经聊过 Response::prepare() 这个方法,此方法能保证响应是严格遵守http协议的.如果能在响应客户端之前,都调用一次这个方法,那有多好。其实这便是 ResponseListener 所做的事情:

$dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));

是不是很方便呢?让我们再添加一个新功能,让响应对象支持响应流(streamed response)功能如何?只用注册一个 StreamedResponseListener 就行了:

$dispatcher->addSubscriber(new HttpKernel\EventListener\StreamedResponseListener());

然后在你的控制器里返回一个 StreamedResponse 实例来替代以前的 Response 实例

请阅读 Symfony 组件的 Internal 这一章,了解更多利用 HttpKernel 做事件分发的知识点,以及他们是如何改变对请求的处理流程的

现在让我们在添加一个可以允许控制器仅返回字符串,而非必须返回一个完整的响应对象的监听器:

class LeapYearController
{
    public function indexAction(Request $request, $year)
    {
        $leapyear = new LeapYear();
        if ($leapyear->isLeapYear($year)) {
            return 'Yep, this is a leap year! ';
        }

        return 'Nope, this is not a leap year.';
    }
}

为了实现此功能,我们需要监听内核里面的 kernel.view 事件,此事件会在控制器刚被调用结束后触发。它的作用是仅在需要的时候,将控制器返回的字符串转化成一个完全的响应对象:

<?php

// example.com/src/Simplex/StringResponseListener.php

namespace Simplex;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;

class StringResponseListener implements EventSubscriberInterface
{
    public function onView(GetResponseForControllerResultEvent $event)
    {
        $response = $event->getControllerResult();

        if (is_string($response)) {
            $event->setResponse(new Response($response));
        }
    }

    public static function getSubscribedEvents()
    {
        return array('kernel.view' => 'onView');
    }
}

只有当控制器返回不是响应对象的时候,此事件才会被触发,另外如果在此事件上设置了响应对象,便停止了此事件的继续传播(原文还有一句,说正因为如此所以代码很简单,但是我觉得这代码简不简单没什么直接逻辑,不知道作者什么意思)

当然别忘记在前端控制器里面注册它

$dispatcher->addSubscriber(new Simplex\StringResponseListener());

如果你忘记了注册(译者注:StringResponseListener),HttpKernel 会抛出一个带有很好错误提示的异常:The controller must return a response (Nope, this is not a leap year. given)..

至此,我们的框架代码是越来越紧凑,而且都是用现成的库组成的。扩展的本质其实就是事件监听+订阅事件。

但愿你现在能够理解为什么这看起来那么简单的 HttpKernelInterface 会这么的强大。它默认实现 HttpKernel,能让你感受到很多的很酷的特性,而且能毫不费劲得使用它。又因为 HttpKernel 是 Symfony 以及 Silex 框架的核心,所以你可以有两全其美的选择:在一个稳固的,持续维护的,并且在许多网站都已验证的地基架构上,做一个可定制的,满足你需求的框架,以及写出做过安全审计而又能很好扩展的代码。

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

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

微信赞赏码

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

7 Comments

elinx

五月 16, 2017 在 9:39 上午

use \Symfony\Component\Debug\Exception\FlattenException instead of \Symfony\Component\HttpKernel\Exception\FlattenException

    Chris Yue

    五月 16, 2017 在 12:10 下午

    thanks

     

sfer

三月 13, 2016 在 9:55 下午

在Symfony HttpKernel3.0.3版本中:

HttpKernel\Exception\FlattenException 路径的FlattenException文件已经不存在了,

不知道该函数与这个函数是同一个函数不?

Symfony\Component\Debug\Exception\FlattenException

    Chris Yue

    三月 13, 2016 在 9:58 下午

    我觉得显然不是,下一个FlatterException是在debug Component里的,跟HttpKernel没什么关系

    对于3.0的变化,我觉得这个你直接抛出\Exception

     

    sfer

    三月 14, 2016 在 9:49 下午

    Debug Component 里面的FlatterException,是继承的这个接口Symfony\Component\HttpKernel\Exception\HttpExceptionInterface

     

sfer

三月 13, 2016 在 9:19 下午

 

Symfony2.8版本(或者更早版本)之后,RouterListener函数有2个必填参数,所以下面这句还缺少一个参数,请问如何实现ReqestStack及配合使用呢?

$dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));

 

class RouterListener implements EventSubscriberInterface
{
    private $matcher;
    private $context;
    private $logger;
    private $requestStack;

    /**
     * Constructor.
     *
     * @param UrlMatcherInterface|RequestMatcherInterface $matcher      The Url or Request matcher
     * @param RequestStack                                $requestStack A RequestStack instance
     * @param RequestContext|null                         $context      The RequestContext (can be null when $matcher implements RequestContextAwareInterface)
     * @param LoggerInterface|null                        $logger       The logger
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($matcher, RequestStack $requestStack, RequestContext $context = null, LoggerInterface $logger = null)

    sfer

    三月 13, 2016 在 9:53 下午

    已经直接new了一个RequestStack

     

发表评论

84 − 75 =