Компонент Routing

Дата оновлення перекладу 2023-06-22

Компонент 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 <?php echo htmlspecialchars(isset($name) ? $name : 'World', ENT_QUOTES, 'UTF-8') ?>

Тепер ми готові до додавання нових функцій.

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

Щоб підтримати цю функцію, додайте компонент Маршрутизація Symfony в якості залежності:

1
$ composer require symfony/routing

Замість масиву для мапи URL, компонент Маршрутизація покладається на екземпляр 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

Прочитайте документацію компонента Маршрутизація, щоб дізнатися більше про його головні функції на кшталт генерування 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 рядками коду, у нас є новий фреймворк, потужніший та гнучкіший, ніж попередній. Насолоджуйтесь!

Використання компонента Маршрутизація має один великий додатковий плюс: здатність генерувати 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);