支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
你可能认为,我们的框架已是无比坚固了,你可能是对的。虽然如此,我们还是来看看如何改进它。
现在,我们的全部例子都是使用面向过程的代码,但要记住,控制器可以是任何有效的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.');
}
} |
更新相应的路由定义:
这种转移是极易理解的,当你创建更多页面时,它变得意义重大,但你可能已经注意到一个不想要的“反作用”……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 创作共用授权。