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

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

Додаємо вимоги

Давайте швиденько оглянемо усі ті маршрути, що були до цього створені. 

Annotations

// src/AppBundle/Controller/BlogController.php

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

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

YAML

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

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" path="/blog/{page}">
        <default key="_controller">AppBundle:Blog:index</default>
        <default key="page">1</default>
    </route>

    <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', new Route('/blog/{page}', array(
    '_controller' => 'AppBundle:Blog:index',
    'page'        => 1,
)));

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

return $collection;

Ви помітили, що не так? Зауважте, що обидва маршрути мають шаблони, які відповідають URL-ам типу /blog/*. Маршрутизатор Symfony завжди обиратиме перший відповідний маршрут, який знайде. Іншими словами, маршрут blog_show у цьому випадку ніколи не вважатиметься відповідним. Натомість, URL на кшталт /blog/my-blog-post відповідатиме вимогам нашого першого маршруту (blog), але від my-blog-post до параметра {page} надійде відповідь про те, що даний елемент не дає зворотної відповіді.

  URL   Маршрут   Параметри
  /blog/2   blog   {page} = 2
  /blog/my-blog-post   blog   {page} = "my-blog-post"

 

 

 

 

Вирішити таку проблему допоможе просте додавання до маршрутів вимог або ж умов (requirements and conditions). Читайте про це детальніше у підрозділі "Повне налаштування відповідності маршрутів та умов" даного розділу. У даному прикладі маршрути ідеально працювали б у тому випадку, коли б шлях /blog/{page} відповідав виключно URL-ам, у яких частина {page} виступає цілим натуральним числом (тобто, має формат integer). На щастя, до кожного з параметрів можна запросто додати певні вимоги у вигляді регулярних виразів (regular expressions). Аби не бути голослівними, наведемо приклад:

Annotations

// src/AppBundle/Controller/BlogController.php

// ...

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

YAML

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

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>
        <requirement key="page">\d+</requirement>
    </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,
), array(
    'page' => '\d+',
)));

return $collection;

Вимога \d+ якраз і є регулярним виразом та визначає цифровий формат значення параметра {page}. Маршрут blog усе так же вважається відповідним для URL-ів на кшталт /blog/2 (через те, що 2 є цифрою), проте віднині він не буде відповідати URL-ам типу /blog/my-blog-post (так як my-blog-post не є числом як таким).

В результаті, URL типу /blog/my-blog-post цілком і повністю відповідатиме маршруту blog_show.

  URL   Маршрут   Параметри
  /blog/2   blog   {page} = 2
  /blog/my-blog-post   blog_show   {slug} = my-blog-post
  /blog/2-my-blog-post   blog_show   {slug} = 2-my-blog-post

 

 

 

 

! Більш ранні маршрути мають значну перевагу

Що це, власне, означає? А означає це лиш те, що порядок маршрутів надзвичайно важливий. Скажімо, якби маршрут blog_show знаходився вище від маршруту blog, то URL /blog/2 відповідав би саме маршруту blog_show, а не blog, оскільки параметр {slug} у blog_show не має ніяких вимог. Таким чином, використовуючи належний порядок та встановлюючи правильні вимоги, ви можете здійснювати практично усе що завгодно!

Оскільки вимоги до параметрів є регулярними виразами, саме від вас залежатиме, наскільки складною та гнучкою буде кожна окремо взята вимога. Припустімо, домашня сторінка вашого додатку доступна 2 різними мовами в залежності від URL:

Annotations

// src/AppBundle/Controller/MainController.php

// ...
class MainController extends Controller
{
    /**
     * @Route("/{_locale}", defaults={"_locale": "en"}, requirements={
     *     "_locale": "en|fr"
     * })
     */
    public function homepageAction($_locale)
    {
    }
}

YAML

# app/config/routing.yml
homepage:
    path:      /{_locale}
    defaults:  { _controller: AppBundle:Main:homepage, _locale: en }
    requirements:
        _locale:  en|fr

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="homepage" path="/{_locale}">
        <default key="_controller">AppBundle:Main:homepage</default>
        <default key="_locale">en</default>
        <requirement key="_locale">en|fr</requirement>
    </route>
</routes>

PHP

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

$collection = new RouteCollection();
$collection->add('homepage', new Route('/{_locale}', array(
    '_controller' => 'AppBundle:Main:homepage',
    '_locale'     => 'en',
), array(
    '_locale' => 'en|fr',
)));

return $collection;

Для вхідних запитів частина URL {_locale} відповідає регулярному виразу (en|fr).

  Маршрут   Параметри
  /   {_locale} = "en"
  /en   {_locale} = "en"
  /fr   {_locale} = "fr"
  /es   не відповідає даному маршруту

 

 

 

 

 

Додаємо вимоги до HTTP-методу

Окрім URL-ів, ви можете перевірити на сумісність і метод вхідного запиту (а саме GET, HEAD, POST, PUT, DELETE). Припустімо, у вас є контактна форма з двома контролерами: один слугує для відображення самої форми (на запит GET), а інший — для обробки форми тоді, коли її заповнено та відправлено (на запит POST). Цю “процедуру” можна здійснити з наступними налаштуваннями маршруту:

Annotations

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

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
// ...

class MainController extends Controller
{
    /**
     * @Route("/contact")
     * @Method("GET")
     */
    public function contactAction()
    {
        // ... display contact form
    }

    /**
     * @Route("/contact")
     * @Method("POST")
     */
    public function processContactAction()
    {
        // ... process contact form
    }
}

YAML

# app/config/routing.yml
contact:
    path:     /contact
    defaults: { _controller: AppBundle:Main:contact }
    methods:  [GET]

contact_process:
    path:     /contact
    defaults: { _controller: AppBundle:Main:processContact }
    methods:  [POST]

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="contact" path="/contact" methods="GET">
        <default key="_controller">AppBundle:Main:contact</default>
    </route>

    <route id="contact_process" path="/contact" methods="POST">
        <default key="_controller">AppBundle:Main:processContact</default>
    </route>
</routes>

PHP

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

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

$collection->add('contact_process', new Route('/contact', array(
    '_controller' => 'AppBundle:Main:processContact',
), array(), array(), '', array(), array('POST')));

return $collection;

Незважаючи на те, що обидва маршрути мають ідентичні шляхи (/contact), перший маршрут відповідатиме лише запитам GET, в той час як другий — виключно запитам POST. Це означає, що і відображати форму, і відправляти її можна через ідентичний URL, у той же час використовуючи різні контролери для двох дій.

! Якщо ви не зазначите ніяких методів methods, в такім разі усі методи будуть відповідати нашому маршруту.

Додаємо вимоги Host

Ви можете ставити у відповідність і HTTP host (надалі хост) вхідного запиту. Детальніше про це можна дізнатися у книзі "Компоненти Symfony" (розділ "Як ставити у відповідність маршрут на основі хосту")

Повне налаштування відповідності маршрутів та умов

Як ви уже помітили, маршрут можна побудувати так, аби він відповідав певним визначеним метазнакам маршрутизації (через регулярні вирази), HTTP-методам або ж хост іменам. Проте гнучкість системи маршрутизації може стати фактично безмежною завдяки використанню умов (conditions):

YAML

contact:
    path:     /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact }
    condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"

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="contact" path="/contact">
        <default key="_controller">AcmeDemoBundle:Main:contact</default>
        <condition>context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'</condition>
    </route>
</routes>

PHP

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('contact', new Route(
    '/contact', array(
        '_controller' => 'AcmeDemoBundle:Main:contact',
    ),
    array(),
    array(),
    '',
    array(),
    array(),
    'context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"'
));

return $collection;

Умова (condition) — це вираз, про синтакс якого можна дізнатися більше у книзі "Компоненти Symfony" в розділі "Синтакс виразів". З умовою шлях не стане у відповідність, допоки HTTP-метод не буде або GET, або HEAD та допоки заголовок User-Agent не відповідатиме firefox.

Таким чином, вам під силу оперувати логікою виразів будь-якого рівня складності, по-максимуму використовуючи дві змінні шляхом застосування їх у самих виразах:

1) context

Видозміна RequestContext, що містить у собі найзагальніші дані про той маршрут, із яким відбувається встановлення відповідності.

2) request

Об’єкт Request у Symfony (див. детальніше у книзі "Компоненти Symfony" за посиланням).

! При створенні URL-ів умови до уваги не беруться.

! Вирази підпорядковуються PHP

Якщо поглянути на ситуацію зсередини, самі вирази зводяться до сирого PHP. Наш приклад нижче згенерує в директорії кешу наступний PHP:

if (rtrim($pathinfo, '/contact') === '' && (
    in_array($context->getMethod(), array(0 => "GET", 1 => "HEAD"))
    && preg_match("/firefox/i", $request->headers->get("User-Agent"))
)) {
    // ...
}

Через це використання ключа condition не тягне за собою додаткових витрат у часі на розпізнавання коду PHP, що лежить в основі, та його безпосереднього виконання.

Маршрутизація вищого рівня у прикладах

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

Annotations

// src/AppBundle/Controller/ArticleController.php

// ...
class ArticleController extends Controller
{
    /**
     * @Route(
     *     "/articles/{_locale}/{year}/{title}.{_format}",
     *     defaults={"_format": "html"},
     *     requirements={
     *         "_locale": "en|fr",
     *         "_format": "html|rss",
     *         "year": "\d+"
     *     }
     * )
     */
    public function showAction($_locale, $year, $title)
    {
    }
}

YAML

# app/config/routing.yml
article_show:
  path:     /articles/{_locale}/{year}/{title}.{_format}
  defaults: { _controller: AppBundle:Article:show, _format: html }
  requirements:
      _locale:  en|fr
      _format:  html|rss
      year:     \d+

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="article_show"
        path="/articles/{_locale}/{year}/{title}.{_format}">

        <default key="_controller">AppBundle:Article:show</default>
        <default key="_format">html</default>
        <requirement key="_locale">en|fr</requirement>
        <requirement key="_format">html|rss</requirement>
        <requirement key="year">\d+</requirement>

    </route>
</routes>

PHP

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

$collection = new RouteCollection();
$collection->add(
    'article_show',
    new Route('/articles/{_locale}/{year}/{title}.{_format}', array(
        '_controller' => 'AppBundle:Article:show',
        '_format'     => 'html',
    ), array(
        '_locale' => 'en|fr',
        '_format' => 'html|rss',
        'year'    => '\d+',
    ))
);

return $collection;

Як ви, напевно, помітили, даний маршрут буде відповідним лиш у тому випадку, якщо частина URL {_locale} буде або en, або fr та якщо частина {year} матиме числовий формат. Даний маршрут також демонструє нам використання між заповнювачами крапок замість скісної риски. Відповідні до такого марштуру URL-и матимуть приблизно наступний вигляд:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss
  • /articles/en/2013/my-latest-post.html

 

! Особливий параметр маршрутизації _format

У даному прикладі особливий наголос робився і на спеціальному параметрі маршрутизації _format. При його використанні відповідний параметр стає "форматом запиту" об’єкта Request. В кінцевому рахунку, формат запиту використовується у таких випадках як при налаштуванні Content-Type відповіді (для прикладу, формат запиту json трансформується у Content-Type елемента application/json). Даний параметр можна також застосовувати у контролері для відображення різних шаблонів кожного значення _format. Цей параметр є дуже потужним способом відображення одного і того ж контенту в різних форматах.

! Інколи вам потрібно зробити так, аби визначені частини маршрутів могли глобально налаштовуватися. Symfony надає вам таку можливість, адже існують параметри сервісу контейнерів (service container parameters). Читайте про це більше за посиланням.

Особливі параметри маршрутизації

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

1) _controller

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

2) _format

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

3) _locale

Використовується для налаштування локалі запиту (детальніше - в оригінальній книзі за посиланням).

Шаблон найменування контролера

Для кожного маршруту повинен бути параметр _controller для визначення того, який саме контролер повинен виконуватися при встановленні відповідності з маршрутом. Цей параметр використовує простий рядковий шаблон під назвою логічне ім'я контролера (logical controller name), котрому Symfony ставить у відповідність конкретний PHP-метод та клас. Шаблон складається із трьох частин, кожна з яких відокремлюється двокрапкою:

bundle:controller:action

Наприклад, _controller у AppBundle:Blog:show означатиме:

  Пакет   Клас контролера   Назва методу
  AppBundle   BlogController   showAction

 

 

 

Контролер може мати наступний вигляд:

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

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        // ...
    }
}

Зауважте, що Symfony додає рядок Controller до назви класу (Blog => BlogController), а також рядок Action до назви методу (show => showAction).

Ви також можете посилатися на цей контролер, використовуючи повне ім'я класу та методу: AppBundle\Controller\BlogController::showAction. А якщо ви скористаєтеся кількома простими умовностями, логічне ім'я стане коротшим та значно гнучкішим.

! Окрім використання логічного імені чи повного імені класу, Symfony надає нам можливість посилатися на контролер ще в один, уже третій, спосіб. Даний метод полягає у використанні двокрапки-розділювача (скажімо, service_name:indexAction) і посилається на контролер як на сервіс (детальніше читайте за посиланням).

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

Поділитися