使用 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

5月 16, 2017 在 9:39 上午

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

 回复

    Chris Yue

    5月 16, 2017 在 12:10 下午

    thanks

     

sfer

3月 13, 2016 在 9:55 下午

在Symfony HttpKernel3.0.3版本中:

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

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

Symfony\Component\Debug\Exception\FlattenException

 回复

    Chris Yue

    3月 13, 2016 在 9:58 下午

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

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

     

    sfer

    3月 14, 2016 在 9:49 下午

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

     

sfer

3月 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

    3月 13, 2016 在 9:53 下午

    已经直接new了一个RequestStack

     

发表评论

1 + 5 =

2021年4月
 1234
567891011
12131415161718
19202122232425
2627282930  
Composer CSS Firefox industrial metal JavaScript LeetCode Linux MySQL NGINX nu metal OAuth PHP PHP7 Shell Symfony Ubuntu Vim 全栈 到底系列 命令行 安全 教程 框架 算法 翻译