
Додаємо вимоги
Давайте швиденько оглянемо усі ті маршрути, що були до цього створені.
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-и матимуть приблизно наступний вигляд:
! Особливий параметр маршрутизації _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) і посилається на контролер як на сервіс (детальніше читайте за посиланням).
У наступних блогах ми продовжимо обговорювати всі особливості маршрутизації. Слідкуйте за нашими найближчими оновленнями!