Маршрутизация

Каждое серьёзное приложение должно обязательно иметь “красивые” URL. Это означает, что это приложение должно оставить позади страшненькие URL типа index.php?article_id=57 в пользу таких /read/intro-to-symfony.

Однако гибкость в этом вопросе - ещё более важна, нежели красота. Что, если вам нужно изменить URL /blog на /news? Сколько ссылок вам придётся отыскать и обновить для этого? Если же вы используете маршрутизатор Symfony, подобные изменения делать легко.

Маршрутизатор Symfony2 позволяет вам определить креативные URL, которые вы сможете привязать к различным областям вашего приложения. По прочтению этой главы вы сможете делать следующее:

  • Создавать сложные маршруты, соответствующие контроллерам;
  • Генерировать URL в шаблонах и контроллерах;
  • Загрузить ресурсы для маршрутизации из пакетов (или из любых других источников);
  • Отлаживать маршруты.

Маршрутизация в действии

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

Шаблон, определяемый маршрутом blog_show работает как выражение /blog/*, где метасимволом является имя slug. Для URL /blog/my-blog-post переменная slug получает значение my-blog-post, которое будет доступно для использования в контроллере.

Параметр _controller - это служебный ключ, который сообщает Symfony, какой именно контроллер должен быть выполнен, когда маршрут совпадает с URL. Строка _controller, называется логическим именем. Логическое имя указывает на некоторый РHP-класс и его метод:

<?php
// src/Acme/BlogBundle/Controller/BlogController.php

namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        $blog = // используйте переменную $slug, для того чтобы получить запись из базы данных

        return $this->render('AcmeBlogBundle:Blog:show.html.twig', array(
            'blog' => $blog,
        ));
    }
}

Поздравляем! Вы только что создали ваш первый маршрут и связали его с контроллером. Теперь, когда вы посетите страницу /blog/my-post, будет выполнен контроллер showAction и переменная $slug будет равна my-post.

Это и есть цель маршрутизатора Symfony2: устанавливать соответствие между URL запроса и контроллером. Далее в этой главе вы узнаете все возможные трюки, которые позволяют легко писать маршруты даже для сложных URL.

Маршрутизация; Что под капотом

Когда создаётся запрос к вашему приложению, он содержит точный адрес ресурса, который запрашивается клиентом. Этот адрес называется URL (или URI) и может выглядеть следующим образом: /contact, /blog/read-me или ещё каким-то похожим образом. Давайте рассмотрим следующий HTTP-запрос в качестве примера:

GET /blog/my-blog-post

Цель системы маршрутизации Symfony2 - разбор этого URL и определение того, какой контроллер должен быть выполнен. Процесс целиком выглядит так:

  1. Запрос обрабатывается фронт-контроллером Symfony2 (например app.php);
  2. Ядро Symfony2 (Kernel), вызывает маршрутизатор для анализа запроса;
  3. Маршрутизатор устанавливает соответствие между входящим URL и некоторым маршрутом и возвращает информацию о маршруте, включая данные о том, какой контроллер требуется выполнить;
  4. Ядро Symfony2 выполняет контроллер, который в конечном итоге возвращает объект Response.
Symfony2 request flow

Слой маршрутизации - это инструмент, который транслирует входящий URL в контроллер, который нужно выполнить для его обработки.

Создание маршрутов

Symfony загружает все маршруты, определённые для вашего приложения, из одного файла настроек. Как правило, этот файл называется app/config/routing.yml, но при желании наименование файла конфигурации можно изменить на другое (в том числе на файл формата XML или PHP) в конфигурационном файле приложения:

Tip

Не смотря на то, что все маршруты загружаются из одного файла, обычной практикой является подключение дополнительных ресурсов внутри этого файла (см. секцию Подключение внешних ресурсов для маршрутизации).

Базовая настройка маршрута

Определить новый маршрут не сложно, типичное приложение будет иметь много различных маршрутов. Самый простой маршрут состоит из двух частей: шаблона URL (pattern) и массива defaults:

Этот маршрут соответствует главной странице (/) и ставит ей в соответствие контроллер AcmeDemoBundle:Main:homepage. Symfony2 переводит строку _controller в имя функции, которую необходимо выполнить. Этот процесс будет объясняться в секции Шаблон Именования Контроллера.

Маршрутизация и Заполнители (Placeholders)

Конечно же система маршрутизации поддерживает и более интересные маршруты. Многие маршруты будут содержать один или более заполнителей (placeholders):

Шаблон будет соответствовать любому URL похожему на /blog/*. Что ещё более важно - значение, соответствующее заполнителю {slug}, будет доступно в вашем контроллере. Другими словами, если дан URL /blog/hello-world, переменная $slug со значением hello-world будет доступна в контроллере. Эту возможность можно использовать, например, для загрузки записи блога, соответствующей этой строке.

Тем не менее, этот шаблон не будет соответствовать URL /blog. Это вызвано тем фактом, что заполнитель по умолчанию является обязательным параметром. Однако, если добавить заполнителю значение по умолчанию в массив defaults.

Обязательные и Опциональные Заполнители

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

Пока что этот маршрут выглядит проще простого - он не содержит заполнителей и соответствует лишь одному URL /blog. Ну а если вам потребуется, чтобы данный маршрут поддерживал постраничную навигацию и URL /blog/2 отображал вторую страницу с записями блога? Добавим к маршруту заполнитель {page}:

Подобно заполнителю {slug}, значение соответствующее {page} будет доступно внутри контроллера. Это значение может быть использовано для того, чтобы определить, какой набор записей блога отобразить для данной страницы.

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

Добавив page в массив defaults, вы сделали заполнитель {page} необязательным. URL /blog будет соответствовать маршруту и значение параметра page будет равно 1. URL /blog/2 также будет соответствовать этому маршруту, присваивая параметру page значение 2. Отлично.

/blog {page} = 1
/blog/1 {page} = 1
/blog/2 {page} = 2

Добавляем Ограничения

Давайте взглянем на маршруты, которые мы добавили ранее:

Можете определить тут проблему? Обратите внимание, что оба маршрута имеют похожие шаблоны и соответствуют URL вида /blog/*. Маршрутизатор Symfony всегда будет выбирать первый совпавший маршрут, который он найдёт. Другими словами, маршрут blog_show никогда не совпадёт и не будет вызван соответствующий контроллер. Вместо этого URL вида /blog/my-blog-post будет соответствовать первому маршруту (blog) и возвращать бессмысленное для параметра {page} значение my-blog-post.

URL route parameters
/blog/2 blog {page} = 2
/blog/my-blog-post blog {page} = my-blog-post

Решением этой проблемы является добавление ограничений в маршрут. Маршруты в этом примере будут работать, если шаблон /blog/{page} будет соответствовать URL лишь в том случае, когда {page} будет целым числом. К счастью, ограничения в виде регулярных выражений легко могут быть добавлены к любому параметру. Например:

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

В результате URL /blog/my-blog-post будет соответствовать маршруту blog_show.

URL route parameters
/blog/2 blog {page} = 2
/blog/my-blog-post blog_show {slug} = my-blog-post

Так как ограничения для параметров - это регулярные выражения, сложность и гибкость каждого ограничения лежит на вашей совести. Предположим, что главная страница вашего приложения доступна на двух различных языках, в зависимости от URL:

Для входящих запросов, часть URL, соответствующая {culture} должна удовлетворять регулярному выражению (en|fr):

/ {culture} = en
/en {culture} = en
/fr {culture} = fr
/es не соответствует маршруту

Добавляем Ограничения для HTTP-метода

В дополнение к URL, вы также можете проверять HTTP-метод входящего запроса (GET, HEAD, POST, PUT, DELETE). Предположим у вас есть форма контактов с двумя контроллерами - один для отображения формы (GET запрос) и другой - для обработки формы, когда она отправлена пользователем (POST запрос). Ограничения для HTTP-метода можно задать следующим образом:

Пренебрегая тем, что оба представленных выше маршрута имеют идентичные шаблоны (/contact), первый маршрут будет соответствовать только GET-запросам, а второй, в свою очередь, будет соответствовать только POST-запросам. Это означает, что вы сможете отображать и отправлять форму, используя один и тот же URL и использовать различные контроллеры для каждого из этих действий.

Note

Если ограничения на _method не указаны, маршрут будет соответствовать любому методу.

Как и любые другие ограничения, ограничения для _method обрабатываются как регулярные выражения. Для того, чтобы соответствовать как GET так и POST запросам, вы можете использовать ограничение GET|POST.

Продвинутая Маршрутизация в Примерах

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

Как вы можете видеть, этот маршрут сработает лишь в том случае, если {culture} в URL будет либо en либо fr и {year} будет числом. Этот маршрут также показывает, что вы можете использовать помимо слэша (/) точку между двумя заполнителями. URL, соответствующий этому маршруту может выглядеть следующим образом:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss

Специальные параметры маршрута

Как вы наверное обратили внимание, каждый параметр маршрута или значение по умолчанию в конечном итоге доступен в виде аргумента в методе контроллера. В дополнение к этому есть также три специальных параметра, каждый из которых добавляет уникальные возможности внутри вашего приложения:

  • _controller: Как вы уже знаете, этот параметр используется для того, чтобы определить какой контроллер будет выполнен, когда маршрут совпадает с URL;
  • _format: Используется для определения запрашиваемого формата (см. параметр маршрута _format);
  • _locale: Используется для того, чтобы установить локаль в сессии (см. локаль в URL);

Шаблон Именования Контроллера

Каждый маршрут должен иметь параметр _controller, который определяет, какой именно контроллер будет выполнен, когда соответствующий маршрут совпадёт с URL. Этот параметр использует простой строковый шаблон, именуемый логическим именем контроллера, которому Symfony ставит в соответствие PHP метод и класс. Шаблон состоит из трёх частей, разделённых двоеточием:

пакет:контроллер:действие

Например, если _controller имеет значение AcmeBlogBundle:Blog:show, то это означает следующее:

Bundle Controller Class Method Name
AcmeBlogBundle BlogController showAction

Контроллер может выглядеть так:

<?php
// src/Acme/BlogBundle/Controller/BlogController.php

namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

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

Обратите внимание, что Symfony добавляет строку Controller у имени класса (Blog => BlogController) и Action к имени метода (show => showAction).

Вы также можете ссылаться на этот класс, используя полное имя класса и метода: Acme\BlogBundle\Controller\BlogController::showAction. Но, если вы следуете нескольким простым соглашениям, логическое имя будет более удобно.

Note

В дополнение к использованию логического имени и полного имени класса, Symfony поддерживает третий тип ссылок на контроллер. Этот метод использует одно двоеточие в качестве разделителя (например service_name:indexAction) и ссылается на котроллер, определённый как сервис (см. /cookbook/controller/service).

Параметры маршрута и Аргументы контроллера

Параметры маршрута (например {slug}) очень важны, так как каждый параметр будет доступен в качестве аргумента в методе-контроллере:

<?php
public function showAction($slug)
{
  // ...
}

Фактически, все defaults объединяются со значениями параметров и формируют один массив. Каждый ключ такого массива доступен в качестве аргумента внутри контроллера.

Другими словами, для каждого аргумента вашего метода-контроллера, Symfony ищет параметр маршрута с тем же именем и присваивает его значение этому аргументу. В продвинутом примере ранее любая комбинация (в любом порядке) следующих переменных может быть использована в качестве аргументов метода showAction():

  • $culture
  • $year
  • $title
  • $_format
  • $_controller

Так как заполнители и массив defaults объединяются, даже переменная $_controller становится доступна. Более подробно это описано в секции Параметры маршрута в качестве аргументов Контроллера.

Tip

Вы также можете использовать переменную $_route, которая содержит имя соответствующего маршрута.

Подключение внешних ресурсов для маршрутизации

Все маршруты загружаются посредством одного конфигурационного файла, обычно это файл app/config/routing.yml (см. Создание маршрутов выше). На практике же вы вероятно захотите загружать маршруты из других мест, например из ваших пакетов. И это становится возможным при помощи “импорта” файла маршрутов:

Note

При импорте ресурсов из YAML, ключ (например acme_hello) не имеет практического значения. Просто убедитесь, что этот ключ уникален и нигде далее не переопределяется.

Ключ resource загружает указанный ресурс с маршрутами. В данном примере ресурс - это полный путь к файлу, где @AcmeHelloBundle это ярлык, означающий путь к пакету. Импортируемый файл может выглядеть следующим образом:

Маршруты из этого файла анализируются и загружаются также, как и основной файл.

Префикс для импортируемого ресурса

Вы также можете указать “префикс” для импортируемого маршрута. Например, предположим, что вы хотите чтобы маршрут acme_hello имел следующий вид: /admin/hello/{name} вместо обычного /hello/{name}:

Строка /admin теперь будет добавлена вначале каждого маршрута, загружаемого из указанного ресурса:

Отображение и Отладка маршрутов

По мере добавления и настройки маршрутов, было бы удобно иметь возможность визуализировать и получать детальную информацию о них. Для того, чтобы увидеть все маршруты вашего приложения, вы можете воспользоваться консольной командой router:debug. Выполните эту команду из корня вашего проекта:

php app/console router:debug

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

homepage              ANY       /
contact               GET       /contact
contact_process       POST      /contact
article_show          ANY       /articles/{culture}/{year}/{title}.{_format}
blog                  ANY       /blog/{page}
blog_show             ANY       /blog/{slug}

Вы также можете получить более подробную информацию о конкретном маршруте, указав его имя после команды router:debug:

php app/console router:debug article_show

Генерация URL

Система маршрутизации также должна позволять генерировать URL. На практике, маршрутизация - это двунаправленная система: устанавливает как соответствие URL с контроллером (+ параметры), так и обратно - превращает маршрут (+ параметры) в URL. Методы :method:`Symfony\\Component\\Routing\\Router::match` и :method:`Symfony\\Component\\Routing\\Router::generate` формируют эту двунаправленную систему. Рассмотрим маршрут blog_show, описанный выше:

<?php
$params = $router->match('/blog/my-blog-post');
// array('slug' => 'my-blog-post', '_controller' => 'AcmeBlogBundle:Blog:show')

$uri = $router->generate('blog_show', array('slug' => 'my-blog-post'));
// /blog/my-blog-post

Для того, чтобы сгенерировать URL, вам необходимо указать имя маршрута (blog_show) и параметры, используемые в шаблоне этого маршрута. Имея эту информацию, можно сгенерировать любой URL:

<?php
class MainController extends Controller
{
    public function showAction($slug)
    {
      // ...

      $url = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post'));
    }
}

В следующей секции вы узнаете как генерировать URL в шаблоне.

Tip

Если фронтэнд вашего приложения использует AJAX, вы возможно захотите иметь возможность генерировать URL в JavaScript при помощи вашей конфигурации маршрутизатора. И вы таки можете это делать при помощи пакета FOSJsRoutingBundle:

var url = Routing.generate('blog_show', { "slug": 'my-blog-post});

Подробнее читайте в документации пакета.

Генерация Абсолютных URL

По умолчанию, маршрутизатор генерирует относительные URL (например /blog). Для того, чтобы сгенерировать абсолютный URL, просто укажите “true” в качестве третьего аргумента метода generate():

<?php
$router->generate('blog_show', array('slug' => 'my-blog-post'), true);
// http://www.example.com/blog/my-blog-post

Note

Хост, который используется для генерации абсолютного URL - это хост из текущего объекта Request. Этот параметр определяется автоматически, основываясь на информации о сервере, которую предоставляет PHP. При создании абсолютных URL для скриптов, запущенных из командной строки, вам необходимо вручную установить желаемый хост для объекта Request:

$request->headers->set('HOST', 'www.example.com');

Генерация URL содержащих строку запроса (Query String)

Метод generate принимает массив значений для генерации URL. Если вы передадите лишний (не указанный в определении маршрута) параметр, он будет добавлен как query string:

$router->generate('blog', array('page' => 2, 'category' => 'Symfony'));
// /blog/2?category=Symfony

Генерация URL в шаблоне

Типичное место, где вам потребуется генерировать URL - это шаблон. Выполнить эту операцию можно, воспользовавшись функцией-помощником:

Абсолютные URL также можно генерировать, но уже при помощи другой функции:

Заключение

Маршрутизатор - это система, ставящая в соответствие URL из входящего запроса контроллеру, который будет вызван для обработки запроса. Он позволяет использовать в приложении “красивые” URL и поддерживать приложение независимым от URL-ов. Маршрутизация - это двунаправленный механизм, и позволяет также генерировать URL.

Дополнительная информация из Книги Рецептов

  • /cookbook/routing/scheme