Компонент HttpKernel: Розвʼязувач контролера

Дата оновлення перекладу 2022-11-09

Компонент HttpKernel: Розвʼязувач контролера

Ви можете подумати, що наш фреймворк вже достатньо цілісний, і ви скоріш за все праві. Але давайте все одно подивимосся, як ми можемо його покращити.

Зараз, всі наші параметри використовують процедурний код, але памʼятайте, що контролери можуть бути будь-якими валідними зворотними викликами PHP. Давайте конвертуємо наш контролер у правильний клас:

1
2
3
4
5
6
7
8
9
10
11
class LeapYearController
{
    public function index($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}', [
    'year' => null,
    '_controller' => [new LeapYearController(), 'index'],
]));

Переміщення достатньо прямолінійне та має чіткий сенс, як тільки ви створите більше ссторінок, але ви могли помітити небажаний побічний ефект... Клас LeapYearController завжди інстанціюється, навіть якщо запитуваний URL не відповідає маршруту leap_year.
Це погано з однієї причини: з точки зору продуктивності, всі контролери для всіх маршрутів мають тепер бути інстанційовані на кожний запит. Було б краще, якщо б контролери мали ліниве завантаження, щоб інстанціювався лише контролер, повʼязаний з маршрутом, що співпав.

Шоб вирішити цю проблеми, а також купу інших, давайте встановимо та використаємо компонент:

1
$ composer require symfony/http-kernel

Компоненет HttpKernel має багато цікавих функцій, але ті, які нам потрібні прямо зараз, - це розвʼязувач контролера та розвʼязувач аргументу. Розвʼязувач контролера знає, як визначити контролер для виконання, а розвʼязувач агументу визначає, в які агрументи його передати, засновуючись на обʼєкті запиту. Всі розвʼязувачі контролера реалізують наступний інфтерфейс:

1
2
3
4
5
6
7
namespace Symfony\Component\HttpKernel\Controller;

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

Метод getController() покладається на ту ж угоду, як і та, що ми визначили раніше: атрибут запиту _controller повинен містити контролер, повʼязаний із запитом. Окрім вбудованих зворотних викликів PHP, getController() також підтримує рядки, що складаються з імені класу, за яким йде дві двокрапки та імʼя методу, в якості валідного зворотного виклику, на кшталт class::method:

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

Щоб цей код працював, змініть код фреймворку, щоб він використовував розвʼязувач контролера з HttpKernel:

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

Note

В якості додаткового бонусу, розвʼязувач належним чином справляється з управлінням помилок за вас: коли ви забуваєте визначити атрибут _controller для маршруту, наприклад.

Тепер, давайте подивимося, як вгадуються аргументи контролера. getArguments() вникає у підпис контролера, щоб визначити, які аргументи передати йому, використовуючи рідний для PHP reflection. Цей метод визначено у настпному інтерфейсі:

1
2
3
4
5
6
7
namespace Symfony\Component\HttpKernel\Controller;

// ...
interface ArgumentResolverInterface
{
    public function getArguments(Request $request, $controller);
}

Метод indexAction() вимагає обʼєкт запиту в якості аргументу. getArguments() знає, коли його правильно впровадити, якщо він коректно типізований:

1
2
3
4
public function index(Request $request)

// не спрацює
public function index($request)

Більш того, getArguments() також у змозі впорвадити будь-який атрибут запиту; аргументі просто необхідно мати те ж імʼя, що і у відповідного атрибуту:

1
public function indexAction($year)

Ви також можете впровадити запит та деякі атрибути одночасно (так як співставлення проходить на рівні імені аргументу або типізування, порядок аргументів не має значення):

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

Розвʼязувачі також беруть на себе клопоти з валідації викликаного контролера та його аргументів. У випадку проблем, він видає виключення з гарним повідомленням, що пояснює проблему (клас контролера не існує, метод не визначено, аргумент не має атрибуту, що співпадає, ...).

Note

З величезною гнучкістю розвʼязувача контролера та розвʼязувача аргументу за замовчуванням, вам може бути цікаво, чому хтось може хотіти створити ще один (чому буде інтерфейс, якщо цього не зробити?) Два приклади: у Symfony,
getController() посилюється для підтримки контролерів в якості сервісів; а аргумент 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
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;

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 $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->send();

Подуиайте про це ще раз: наш міцніший та гнучкіший, ніж будь-коли, і він все ще має менше 50 рядків коду.