Http内核组件: 控制器解析器

你可能认识我们的框架已经足够健壮,说的没错,但是尽管如此,还是让我们来看看有什么可以改进的地方。

目前为止,我们所有的实例都用的面向过程的代码,但是请记住我们的控制器可以是任何合法的PHP回调。让我们将控制器转换成一个像样的类:

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.');
    }
}

相应的,更新路由定义:

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

这个改动非常简单,在创建更多页面时你会发现这很有意义,但是你可能没有注意到的是,它存在一个不明显的负面作用...LeapYearController总是被实例化,尽管请求的URL没有匹配到leap_year 路由。这对于高效性能这个要点来说是不利的,对于每一个单独的请求所有路由的控制器现在都会被实例化。如果控制器是懒加载的就更好了,这样就只有和路由匹配的控制器会被实例化。

为了解决这个问题,以及其他的一堆,让我们安装并使用HttpKernel组件:

$ composer require symfony/http-kernel

HttpKernel组件有很多有趣的特性,但是目前我们需要的是 控制器解析器 and 参数解析器。一个控制器解析器决定控制器的执行,参数解析器决定传递给它基于请求对象的参数。所有的控制器解析器都实现了下面的接口:

namespace Symfony\Component\HttpKernel\Controller;

// ...
interface ControllerResolverInterface
{
    function getController(Request $request);

    function getArguments(Request $request, $controller);
}

getArguments()放在Symfony 3.1.* 版本中已经被弃用,即将在4.0版本后移除。你可以使用实现了 ArgumentResolverInterfaceArgumentResolver 作为替代。

getController() 方法和我们之前定义的依赖的是同一个原则:请求属性 _controller 必须包含与请求相关联的控制器。出了内置的PHP回调,getController()也支持由类名称后跟两个冒号以及一个合法的回调方法名称组成的字符串,例如 "class::method":

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

为了使代码生效,用HttpKernel的控制器解析器来修改框架的代码:

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);

另外,控制器解析器会在适当的时候帮你处理错误:例如当你忘记为路由定义一个_controller属性的时候。

现在,让我们看看控制器的参数是如何获取的。getArguments()使用了PHP原生的反射来检查控制器的特征,以此决定哪些参数应该被传递。

indexAction() 方法需要一个请求对象的参数,getArguments()方法会根据类型暗示来判断何时去注入请求对象:

public function indexAction(Request $request)

// won't work
public function indexAction($request)

更有意思的是,getArguments() 也可以注入任何的请求属性;参数名称要与相应的属性同名:

public function indexAction($year)

你可以同时注入请求以及一些属性(匹配的原则是根据类型暗示和参数名称,跟参数的顺序无关):

public function indexAction(Request $request, $year)

public function indexAction($year, Request $request)

最后,你还能给任何匹配的属性赋予一个默认值:

public function indexAction($year = 2012)

让我们的控制器里注入一个$year:

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.');
    }
}

解析器会同时验证控制器可否调用以及它的参数。一旦遇到了问题,解析器会抛出一个具有提示错误信息的异常(控制器类不存在,方法未被定义,参数没有可匹配的属性等等...)。

有了默认的控制器解析器和参数解析器强大的灵活性,你可能想知道为什么某些人会想去创建另外的(为什么这里是一个接口或者没有会怎样)。2个例子:在Symfony中,getController()强大到可以支持 controllers as servicesgetArguments()提供了一个改变后者增强参数解析的扩展点。

让我们以新版的框架作为结束:

// 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代码以内。

results matching ""

    No results matching ""