Вы наверное думаете, что наш фреймворк уже достаточно хорош, и вы правы! Но, тем не менее, давайте посмотрим, как мы можем сделать его еще лучше.
Сейчас все наши примеры используют процедурный код, но помните, что контроллеры могут быть любыми валидными объектами типа 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('Да, это високосный год!'); } return new Response('Нет, это не високосный год.'); } } |
И, соответственно, обновим определение маршрута.
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. И это очень плохо для производительности. Все контроллеры для всех маршрутов имеют экземпляр, вне зависимости от запроса. Было бы гораздо лучше, если бы был определен только контроллер, ассоциированный с нужным маршрутом.
Что бы решить эту задачу, и многие другие, мы установим и используем компонент HttpKernel:
1 2 3 4 5 6 7 8 |
{ "require": { "symfony/class-loader": "2.1.*", "symfony/http-foundation": "2.1.*", "symfony/routing": "2.1.*", "symfony/http-kernel": "2.1.*" } } |
Компонент HttpKernel имеет много интересных возможностей, но прямо сейчас нам нужен controller resolver. Controller resolver знает как определять какой контроллер запускать с какими параметрами, основываясь на объекте Request.
Все резолверы контроллеров реализуют следующий интерфейс:
1 2 3 4 5 6 7 8 |
namespace Symfony\Component\HttpKernel\Controller; interface ControllerResolverInterface { function getController(Request $request); function getArguments(Request $request, $controller); } |
Метод getController() опирается на те же самые правила, что и тот, который мы определили ранее: параметр _controller должен содержать контроллер, ассоциированный с запросом. Также ф-ция getController() может принимать строку, составленную из имени класса и имени метода через два двоеточия, как ‘class::method‘:
1 2 3 4 |
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( 'year' => null, '_controller' => 'LeapYearController::indexAction', ))); |
Изменим код фреймворка для использования controller resolver из компонента HttpKernel.
1 2 3 4 5 6 7 8 |
use Symfony\Component\HttpKernel; $resolver = new HttpKernel\Controller\ControllerResolver(); $controller = $resolver->getController($request); $arguments = $resolver->getArguments($request, $controller); $response = call_user_func_array($controller, $arguments); |
Теперь посмотрим, как controller resolver определит параметры контроллера. getArguments() исследует определение контроллера, что бы понять какие параметры в него передавать, используя родное расширение PHP: Reflection.
Метод indexAction() принимает объект Request в качестве параметра. Что бы getArguments() корректно передала его, нужно дать ей небольшую подсказку:
1 2 3 4 |
public function indexAction(Request $request) // Не будет работать public function indexAction($request) |
Более интересно, что getArguments так же может передавать любые параметры запроса. Имя параметра метода просто должно совпадать с именем параметра запроса.
1 |
public function indexAction($year) |
Так же можно передать объект Request и параметры запроса одновременно (и порядок не важен).
1 2 3 |
public function indexAction(Request $request, $year) public function indexAction($year, Request $request) |
И наконец, вы можете задать значение по умолчанию для любого параметра, которого может не быть в запросе.
1 |
public function indexAction($year = 2012) |
Давайте сделаем передачу параметра запроса $year в наш контроллер:
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('Да, это високосный год!'); } return new Response('Нет, это не високосный год.'); } } |
Controller resolver также позаботится о валидации вызываемого контроллера и его аргументов. В случае проблемы, он выдаст исключение с разъяснением проблемы (не удалось найти класс контроллера, не удалось найти класс метода, неверно определены параметры…)
Вы наверно спросите, при такой гибкости controller resolver, зачем кому-то, придет в голову писать свой собственный (ведь существует же интерфейс)?
Два примера:
- В Symfony2, getController() усовершенствован, для поддержки controllers as services;
- В FrameworkExtraBundle getArguments() усовершенствована для поддержки конвертеров параметров, где параметры запроса автоматически конвертируются в объекты.
Итак, новая версия фреймворка:
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 42 |
<?php // framework/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) { extract($request->attributes->all()); 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); $resolver = new HttpKernel\Controller\ControllerResolver(); try { $request->attributes->add($matcher->match($request->getPathInfo())); $controller = $resolver->getController($request); $arguments = $resolver->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(); |
Подумайте только: наш фреймворк, более стабильный, более гибкий чем когда либо, и он все еще занимает меньше 40 строк кода.
К содержанию >>
Оригинал статьи на английском языке >>
Исходный код из статьи >>