Компонент Routing

Дата обновления перевода 2023-06-29

Компонент Routing

Перед тем, как мы нырнём в компонент Routing, давайте немножечко перепроектируем наш текущий фреймворк , чтобы сделать шаблоны ещё более читаемыми:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

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

$request = Request::createFromGlobals();

$map = [
    '/hello' => 'hello',
    '/bye'   => 'bye',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    extract($request->query->all(), EXTR_SKIP);
    include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]);
    $response = new Response(ob_get_clean());
} else {
    $response = new Response('Not Found', 404);
}

$response->send();

Так как теперь мы извлекает параметры запроса, упростите шаблон hello.php следующим образом:

1
2
<!-- example.com/src/pages/hello.php -->
Hello <?= htmlspecialchars($name ?? 'World', ENT_QUOTES, 'UTF-8') ?>

Теперь, мы готовы к добавлению новых функций.

Одним очень важным аспектом любого сайта является форма его URL. Благодаря карте URL, мы отделили URL от кода, генерирующего связанный ответ, но он ещё недостаточно гибок. Например, мы можем захотеть поддержать динамические пути, чтобы разрешить встраивание данных напрямую в URL (например, /hello/Fabien), вместо того, чтобы полагаться на строку запроса (например, /hello?name=Fabien).

Чтобы поддержать эту функцию, добавьте компонент Routing Symfony в качестве зависимости:

1
$ composer require symfony/routing

Вместо массива для карты URL, компонент Routing полагается на экземпляр RouteCollection:

1
2
3
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();

Давайте добавим маршрут, который описывает URL /hello/SOMETHING и добавим ещё оди для простого /bye:

1
2
3
4
use Symfony\Component\Routing\Route;

$routes->add('hello', new Route('/hello/{name}', array('name' => 'World')));
$routes->add('bye', new Route('/bye'));

Каждая запись в коллекции определяется именем (hello) и экземпляром Route, который определяется схемой маршрута (/hello/{name}) и массивом значений по умолчанию для атрибутов маршрута (array('name' => 'World')).

Note

Прочтите документацию компонента Routing, чтобы узнать больше о его главных функциях вроде генерирования URL, требований атрибутов, принуждения HTTP-методов, загрузчиков для файлов YAML или XML, сброса правил вывода в PHP или Apache для улучшенной производительности, и многом другом.

Основываясь на информации, хранящейся в экземпляре RouteCollection, экземпляр UrlMatcher может совпадать с путями URL:

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$context = new RequestContext();
$context->fromRequest($request);
$matcher = new UrlMatcher($routes, $context);

$attributes = $matcher->match($request->getPathInfo());

Метод match() берёт путь запроса и возвращает массив атрибутов (заметьте, что совпадающий маршрут автоматически хранится в специальном атрибуте _route):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$matcher->match('/bye');
/* Result:
[
    '_route' => 'bye',
];
*/

$matcher->match('/hello/Fabien');
/* Result:
[
    'name' => 'Fabien',
    '_route' => 'hello',
];
*/

$matcher->match('/hello');
/* Result:
[
    'name' => 'World',
    '_route' => 'hello',
];
*/

Note

Даже если нам не нужен контекст запроса в наших примерах, он используется в реальных приложениях для принуждения требований методов и другого.

Сопоставитель URL выдаёт исключение, когда не совпадает ни один маршрут:

1
2
3
$matcher->match('/not-found');

// выдаёт Symfony\Component\Routing\Exception\ResourceNotFoundException

В этими знаниями в голове, давайтенапишем новую версию нашего фреймворка:

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

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);

try {
    extract($matcher->match($request->getPathInfo()), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);

    $response = new Response(ob_get_clean());
} catch (Routing\Exception\ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->send();

В коде существует несколько новых вещей:

  • Имена маршрутов используются для имён шаблонов;
  • Ошибки 500 теперь корректно обрабатываются;
  • Атрибуты запросов извлекаются для того, чтобы наши шаблоны оставались простыми:
1
2
// example.com/src/pages/hello.php
Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
  • Конфигурация маршрута была перемещена в собственный файл:

    1
    2
    3
    4
    5
    6
    7
    8
    // example.com/src/app.php
    use Symfony\Component\Routing;
    
    $routes = new Routing\RouteCollection();
    $routes->add('hello', new Routing\Route('/hello/{name}', ['name' => 'World']));
    $routes->add('bye', new Routing\Route('/bye'));
    
    return $routes;

Теперь у нас есть чёткое разделение между конфигурацией (всё, относящееся к нашему приложению в app.php) и фреймворком (общий код, который питает наше приложение, в front.php).

С менее, чем 30 строками кода, у нас есть новый фреймворк, более мощный и гибкий, чем предыдущий. Наслаждайтесь!

Использование компонента Routing имеет один большой дополнпительный плюс: способность генерировать URL, основываясь на определениях Маршрута. При использовании и совпадения URL и генерировании URL в вашем коде, имнение схем URL не должно иметь никакого другого влияния. Хотите узнать, как использовать генератор? Сумасшедше легко:

1
2
3
4
5
6
use Symfony\Component\Routing;

$generator = new Routing\Generator\UrlGenerator($routes, $context);

echo $generator->generate('hello', ['name' => 'Fabien']);
// выводит /hello/Fabien

Код не должен требовать объяснений, и благодаря контексту, вы можете даже сгенерировать абсолютные URL:

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

echo $generator->generate(
    'hello',
    ['name' => 'Fabien'],
    UrlGeneratorInterface::ABSOLUTE_URL
);
// выводит что-то вроде http://example.com/somewhere/hello/Fabien

Tip

Волнуетесь о производительности? Основываясь на ваших определениях маршрутов, создайте высокооптимизированный класс сопоставителя URL, который может заменить UrlMatcher по умолчанию:

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;

// $compiledRoutes - это простой PHP-массив, который описывает все маршруты в формате данных с высокой производительностью
// вы можете (и должны) кешировать его, обычно, путём экспорта в PHP-файл
$compiledRoutes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes();

$matcher = new CompiledUrlMatcher($compiledRoutes, $context);