HttpKernel组件:控制器解析器

你可能认为,我们的框架已是无比坚固了,你可能是对的。虽然如此,我们还是来看看如何改进它。

现在,我们的全部例子都是使用面向过程的代码,但要记住,控制器可以是任何有效的PHP回调(callbacks)。我们来把控制器转换为一个合适的类:

1
2
3
4
5
6
7
8
9
10
11
class LeapYearController
{
    public function indexAction($request)
    {
        if (is_leap_year($request->attributes->get('year'))) {
            return new Response('Yep, this is a leap year!');
        }
 
        return new Response('Nope, this is not a leap year.');
    }
}

更新相应的路由定义:

1
2
3
4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => array(new LeapYearController(), 'indexAction'),
)));

这种转移是极易理解的,当你创建更多页面时,它变得意义重大,但你可能已经注意到一个不想要的“反作用”……LeapYearController类将始终被实例化,哪怕请求的URL并不匹配leap_year路由。这是很糟糕的,主要原因是:性能不灵,全部路由的所有控制器在每次请求时,都要被实例化。如果控制器能被lazy-loaded(懒加载),进而只有那些匹配了路由的控制器被实例化,应该是更好的(方案)。

为了解决这个问题,连同更多问题,我们安装HttpKernel组件并使用它:

1
$  composer require symfony/http-kernel

HttpKernel组件有很多有趣的功能,我们急需的是controller resolver和argument resolver。控制器解析器知道如何决定使用(哪个)控制器,而参数解析器用于决定传给控制器的参数,基于一个Request对象。所有的控制器解析器实现的是以下接口:

1
2
3
4
5
6
7
8
9
namespace Symfony\Component\HttpKernel\Controller;
 
// ...
interface ControllerResolverInterface
{
    function getController(Request $request);
 
    function getArguments(Request $request, $controller);
}

getArguments()方法在Symfony 3.1中被deprecated(不推荐使用),将在4.0中被移除。你可以使用ArgumentResolver,它实现的是ArgumentResolverInterface接口。

getController()方法依赖的命名约定和我们之前定义过的相同:_controller这个request attribute(请求属性)必须包含与Request相关联的控制器。除了内置的PHP回调(callbacks),getController()也支持由“类名后面跟两个冒号和一个方法名”所组成的字符串来作为一个有效的回调,比如“class::method”:

1
2
3
4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => 'LeapYearController::indexAction',
)));

为了让上面的代码能够运行,修改控制器代码,使用HttpKernel的控制器解析器(controller resolver):

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpKernel;
 
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
 
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);
 
$response = call_user_func_array($controller, $arguments);

作为一个奖励,控制器解析器替你安排好了“错误管理”(error management):比如,当你忘记为一个路由定义_controller属性时。

现在,我们来看看,控制器参数是如何被猜出来的。getArguments()在内部检查了控制器签名,通过使用reflection(反射)来决定哪个参数应该传给它(控制器)。

indexAction()方法需要Request对象作为参数。如果该对象被正确地进行了“类型提示”(type-hint),getArguments()方法知道何时正确地注入它:

1
2
3
4
public function indexAction(Request $request)
 
// won't work 不能运行
public function indexAction($request)

更有意思的是,getArguments()还能注入Request的任意属性;作为(控制器的)参数只需使用和对应属性相同的名字即可:

1
public function indexAction($year)

你已经能够在同一时间注入Request对象和一些属性了(由于针对参数名或类型提示的匹配已经完成,参数的顺序是没有要求的):

1
2
3
public function indexAction(Request $request, $year)
 
public function indexAction($year, Request $request)

最后,你可以为任何一个参数定义默认值,用来匹配Request对象中的一个可选属性(optional attribute):

1
public function indexAction($year = 2012)

我们把$year这个request attribute注入到控制器中:

1
2
3
4
5
6
7
8
9
10
11
class LeapYearController
{
    public function indexAction($year)
    {
        if (is_leap_year($year)) {
            return new Response('Yep, this is a leap year!');
        }
 
        return new Response('Nope, this is not a leap year.');
    }
}

解析器负责对控制器回调(译注:symfony的控制器是个callable)及其参数进行验证。如果有问题,它会抛出一个异常,解析问题所在并给出美观信息(比如下面这些:the controller class does not exist, the method is not defined, an argument has no matching attribute, ...)。

默认的控制器解析器和参数解析器已经超级灵活,你可能很奇怪为什么还有人愿意创建另外一个(否则为何要留有interface?)。举两个例子:在Symfony中getController()被强化为支持把控制器作为服务;而getArguments()提供了一个扩展点(extension point),用于修改或强化对参数的解析。

我们总结一下最新版本的框架:

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
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
 
function render_template(Request $request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    return new Response(ob_get_clean());
}
 
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
 
    $controller = $controllerResolver->getController($request);
    $arguments = $argumentResolver->getArguments($request, $controller);
 
    $response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 
$response->send();

请再次确认:我们的框架比以往更健壮,更灵活,它仍然不超过50行代码。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)