Маршрутизація (частина 1)

19/05/2015 0 symfony, маршрутизація, маршрут, маршрутизатор

Будь-який солідний веб-додаток обов’язково повинен мати красиві URL-и. Забудьте про недолугі URL-и на кшталт index.php? Article_id = 57. Натомість використовуйте такі, як, наприклад, /read/intro-to-symfony.

Ще більш важливою характеристикою URL-ів є гнучкість. Що, якщо вам потрібно буде змінити URL сторінки з /blog на /news? Скільки посилань ви змушені будете відстежити і оновити? А от з маршрутизатором Symfony подібні зміни вносяться дуже просто!

Маршрутизатор Symfony дозволяє визначити креативні URL-и, які ви прив’язуєте до різних ділянок свого веб-додатку. До кінця усіх 3 частин розділу про Маршрутизацію ви навчитеся:

1) створювати складні маршрути, які прив’язуються до контролерів

2) генерувати URL-и усередині шаблонів і контролерів

3) завантажувати ресурси для маршрутизації з пакетів (або з інших джерел)

4) налагоджувати маршрути

Маршрутизація в дії

Маршрут - це зв’язна ланка між URL-шляхом і контролером. Наприклад, припустимо, що ви хочете підібрати URL на кшталт /blog/my-post або /blog/all-about-symfony і відправити його контролеру, який знайде і відобразить цей запис блогу. Маршрут простий:

Annotations:

// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class BlogController extends Controller
{
    /**
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function showAction($slug)
    {
        // ...
    }
}

YAML:

# app/config/routing.yml
blog_show:
    path:      /blog/{slug}
    defaults:  { _controller: AppBundle:Blog:show }

XML:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog_show" path="/blog/{slug}">
        <default key="_controller">AppBundle:Blog:show</default>
    </route>
</routes>

PHP:

// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog_show', new Route('/blog/{slug}', array(
    '_controller' => 'AppBundle:Blog:show',
)));

return $collection;

Шлях, визначений маршрутом blog_show, діє як /blog/*, де метасимволом є ім’я slug. Для URL-а /blog/my-blog-post змінна slug отримує значення my-blog-post, доступне для використання вашому контролеру (продовжуйте читати). blog_show - це внутрішнє ім'я маршруту, яке поки не має ніякого значення і просто повинно бути унікальним. Пізніше ви будете використовувати його для створення URL-ів.

Якщо ви не хочете використовувати анотації, тому що вони вам не подобаються або тому що ви не хочете залежати від SensioFrameworkExtraBundle, можна також використовувати YAML, XML або PHP. У цих форматах параметр _controller - це спеціальний ключ, який повідомляє Symfony, який контролер повинен бути виконаний, якщо URL відповідає цьому маршруту. Рядок _controller називається логічним ім'ям (детальніше про це читайте у підрозділі "Шаблон найменування контролера", що за посиланням). Він вказує на певний клас PHP і метод, в даному випадку метод AppBundle\Controller\BlogController::showAction.

Вітаємо! Ви щойно створили свій перший маршрут і підв’язали його до контролера. Тепер, коли ви відвідаєте /blog/my-post, буде виконаний контролер showAction, і змінна $slug дорівнюватиме my-post.

В цьому й полягає мета маршрутизатора Symfony: прив’язати URL запиту до контролера. В процесі читання ви дізнаєтеся всі прийоми, що полегшують прив’язування навіть найскладніших URL-ів.

Маршрутизація: копаємо глибше

Коли зроблено запит до вашого додатку, він містить звернення до точного "ресурсу", який запитує клієнт. Ця адреса називається URL (або URI), і може виглядати як /contact, /blog/read-me або будь-що інше. Візьмемо за приклад наступний запит HTTP:

GET /blog/my-blog-post

Мета системи маршрутизації Symfony - розібрати цей URL і визначити, який контролер повинен бути виконаний. Весь процес виглядає наступним чином:

1) Запит обробляється фронт-контролером Symfony (наприклад app.php);

2) Ядро Symfony (тобто Kernel) просить маршрутизатор дослідити запит;

3) Маршрутизатор підбирає вхідний URL до певного маршруту і повертає інформацію про маршрут, в тому числі про контролер, який повинен бути виконаний;

4) Ядро Symfony виконує контролер, який врешті-решт повертає об'єкт Response.

Symfony request flow

Шар маршрутизації - це інструмент, який переводить вхідний URL в певний контролер, що має бути виконаний.

Створення маршрутів

Symfony завантажує всі маршрути для вашого додатку з одного файлу налаштувань маршрутизатора. Цей файл - зазвичай app/config/routing.yml, але можна налаштувати його будь-чим іншим (у тому числі XML або PHP-файлом) через файл конфігурації додатку:

YAML:

# app/config/config.yml
framework:
    # ...
    router: { resource: "%kernel.root_dir%/config/routing.yml" }

XML:

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <!-- ... -->
        <framework:router resource="%kernel.root_dir%/config/routing.xml" />
    </framework:config>
</container>

PHP:

// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'router' => array(
        'resource' => '%kernel.root_dir%/config/routing.php',
    ),
));

! Навіть якщо всі маршрути завантажуються з одного файлу, звичною практикою є включання додаткових ресурсів маршрутизації. Для цього просто вкажіть в основному файлі налаштувань маршрутизатора, які зовнішні файли повинні потрібно включити. Більше інформації - в розділі про Підключення зовнішніх ресурсів маршрутизації.

Базові налаштування маршруту

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

Annotations:

// src/AppBundle/Controller/MainController.php

// ...
class MainController extends Controller
{
    /**
     * @Route("/")
     */
    public function homepageAction()
    {
        // ...
    }
}
 YAML:
# app/config/routing.yml
_welcome:
    path:      /
    defaults:  { _controller: AppBundle:Main:homepage }
 XML:
 <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="_welcome" path="/">
        <default key="_controller">AppBundle:Main:homepage</default>
    </route>

</routes>

PHP:

 // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('_welcome', new Route('/', array(
    '_controller' => 'AppBundle:Main:homepage',
)));

return $collection;

Цей маршрут збігається з homepage (/) і веде її до контролера AppBundle:Main:homepage. Рядок  _controller переводиться Symfony в реальну функцію PHP і виконується. Цей процес ми коротко пояснимо в підрозділі "Шаблон найменування контролера", що за посиланням).

Маршрутизація і заповнювачі (Placeholders)

Звичайно, система маршрутизації підтримує набагато цікавіші маршрути. Багато маршрутів містять один або більше заповнювачів:

Annotations:

// src/AppBundle/Controller/BlogController.php

// ...
class BlogController extends Controller
{
    /**
     * @Route("/blog/{slug}")
     */
    public function showAction($slug)
    {
        // ...
    }
}

YAML:

 # app/config/routing.yml
blog_show:
    path:      /blog/{slug}
    defaults:  { _controller: AppBundle:Blog:show }

XML:

 <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog_show" path="/blog/{slug}">
        <default key="_controller">AppBundle:Blog:show</default>
    </route>
</routes>

PHP:

// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog_show', new Route('/blog/{slug}', array(
    '_controller' => 'AppBundle:Blog:show',
)));

return $collection;

Шлях буде відповідати будь-якому URL, схожому на /blog/*. Або навіть краще - значення, що відповідає заповнювачу {slug}, буде доступним у вашому контролері. Іншими словами, якщо URL - /blog/hello-world, то змінна $slug, зі значенням hello-world, буде доступна в контролері. Це може бути використано, наприклад, для завантаження запису блогу, що відповідає цьому рядку.

Однак, шлях не відповідатиме URL /blog. Адже, за замовчуванням, потрібні всі заповнювачі. Це може бути змінено шляхом додавання значення заповнювача до масива defaults.

Обов'язкові та опційні заповнювачі

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

Annotations:

 // src/AppBundle/Controller/BlogController.php

// ...
class BlogController extends Controller
{
    // ...

    /**
     * @Route("/blog")
     */
    public function indexAction()
    {
        // ...
    }
}
 YAML:
# app/config/routing.yml
blog:
    path:      /blog
    defaults:  { _controller: AppBundle:Blog:index }

XML:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" path="/blog">
        <default key="_controller">AppBundle:Blog:index</default>
    </route>
</routes>

PHP:

 // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog', array(
    '_controller' => 'AppBundle:Blog:index',
)));

return $collection;
Поки цей маршрут максимально простий - він не містить заповнювачів і відповідатиме тільки точному URL /blog. Але раптом вам знадобиться, щоб він підтримував пагінацію, тобто щоб URL /blog/2 відображав другу сторінку записів у блозі? Оновіть маршрут, щоб отримати новий заповнювач - {page}:

Annotations:

 // src/AppBundle/Controller/BlogController.php

// ...

/**
 * @Route("/blog/{page}")
 */
public function indexAction($page)
{
    // ...
}

YAML:

 # app/config/routing.yml
blog:
    path:      /blog/{page}
    defaults:  { _controller: AppBundle:Blog:index }

XML:

 <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" path="/blog/{page}">
        <default key="_controller">AppBundle:Blog:index</default>
    </route>
</routes>

PHP: 

 // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AppBundle:Blog:index',
)));

return $collection;

Як і у випадку з заповнювачем {slug}, значення, що відповідає {page}, буде доступне у вашому контролері. Це значення може бути використане для визначення того, який набір блогпостів відображати на даній сторінці.

Але зачекайте! Так як заповнювачі потрібні за замовчуванням, цей маршрут більше не відповідатиме просто URL-у /blog. Натомість, щоб побачити 1-у сторінку блогу, вам потрібно використовувати URL /blog/1! Це неприпустимо для гарного веб-додатку, тож змініть маршрут, щоб параметр {page} став опційним. Це можна зробити шляхом включення його в масив defaults:

Annotations:

// src/AppBundle/Controller/BlogController.php

// ...

/**
 * @Route("/blog/{page}", defaults={"page" = 1})
 */
public function indexAction($page)
{
    // ...
}

YAML:

 # app/config/routing.yml
blog:
    path:      /blog/{page}
    defaults:  { _controller: AppBundle:Blog:index, page: 1 }

XML:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" path="/blog/{page}">
        <default key="_controller">AppBundle:Blog:index</default>
        <default key="page">1</default>
    </route>
</routes>

PHP:

// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AppBundle:Blog:index',
    'page'        => 1,
)));

return $collection;

Додавши page в масив defaults, ви більше не матимете потреби в заповнювачі {page}. URL /blog відповідатиме цьому маршруту і значення параметра page буде встановлене як 1. URL /blog/2, у свою чергу, надасть параметру page значення 2. Те, що треба!

URL

Маршрут

Параметри

/blog

  blog

{page} = 1

/blog/1

  blog

{page} = 1

/blog/2

  blog

{page} = 2

! Звичайно, ви можете мати більше, ніж один опційний заповнювач (наприклад, /blog/{slug}/{page}), але все, що після опційного заповнювача, повинно бути опційним. Наприклад, /{page}/blog - валідний шлях, але завжди буде потрібний page (тобто просто URL /blog не відповідатиме цьому маршруту).

! Маршрути з опційними параметрами врешті-решт не відповідатимуть запитам з рискою в кінці (тобто /blog/ не відповідатиме, а /blog відповідатиме).

У наступних блогах ми продовжимо обговорювати всі особливості маршрутизації. Слідкуйте за нашими найближчими оновленнями!

Поділитися