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

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

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

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

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

Примеры маршрутизации

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

  • Annotations
     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
    28
    29
    30
    31
    // src/AppBundle/Controller/BlogController.php
    namespace AppBundle\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class BlogController extends Controller
    {
        /**
         * Точное соответствие /blog
         *
         * @Route("/blog", name="blog_list")
         */
        public function listAction()
        {
            // ...
        }
    
        /**
         * Соответствие /blog/*
         *
         * @Route("/blog/{slug}", name="blog_show")
         */
        public function showAction($slug)
        {
            // $slug будет соответствовать динамической части URL
            // например, при /blog/yay-routing, $slug='yay-routing'
    
            // ...
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/routing.yml
    blog_list:
        path:     /blog
        defaults: { _controller: AppBundle:Blog:list }
    
    blog_show:
        path:     /blog/{slug}
        defaults: { _controller: AppBundle:Blog:show }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- 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_list" path="/blog">
            <default key="_controller">AppBundle:Blog:list</default>
        </route>
    
        <route id="blog_show" path="/blog/{slug}">
            <default key="_controller">AppBundle:Blog:show</default>
        </route>
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route('/blog', array(
        '_controller' => 'AppBundle:Blog:list',
    )));
    $collection->add('blog_show', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:show',
    )));
    
    return $collection;
    

Благодаря этим двум маршрутам:

  • Если пользователь переходит в /blog, первый маршрут подходит и выполняется listAction();
  • Если вользователь переходит в /blog/*, второй маршрут подходит и выполняется showAction(). Так как путь маршрута /blog/{slug}, в showAction() определяется переменная $slug, совпадающая с этим значением. Например, если пользователь переходит на /blog/yay-routing, тогда $slug будет равняться yay-routing.

Если в вашем маршруте есть {placeholder}, тогда эта часть становится метасимволом (wildcard): она подходит под любое значение. Ваш контроллер теперь может также иметь аргумент под названием $placeholder (названия метасимвола и аргумента должны совпадать).

Каждый маршрут также имеет внутренне имя: blog_list и blog_show. Они могут быть любыми (пока каждое из них уникально), и пока не имеют никакого значения. Позже, вы будете использовать их для генерирования URL.

@Route над каждым методом называется аннотация. Если вы предпочитаете создавать ваши маршруты в YAML, XML или PHP, то в этом нет никакой проблемы!

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

логическим именем. Оно указывает на конкретный

РНР-класс и метод. В этом случае, на методы AppBundle\Controller\BlogController::listAction и AppBundle\Controller\BlogController::showAction.

Смысл маршрутизатора Symfony – прокладывать путь от URL запроса к контроллеру. Далее вы изучите всевозможные трюки, которые сделают маршрутизацию даже самых сложных URL простой.

Добавляем ограничения {метасимволы}

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

  • blog_list: /blog/{page} будет подходить под /blog/*;
  • blog_show: /blog/{slug} будет также подходить под /blog/*.

Когда два маршрута подходят под один и тот же URL, первый маршрут, который загружается - выигрывает. К сожалению, это значит, что /blog/yay-routing будет подходить с ``blog_list`. Нехорошо!

Для того, чтобы это исправить, добавьте ограничение, указывающее, что метасимвол {page} может подходить только под URL с набором цифр:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 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/{page}", name="blog_list", requirements={"page": "\d+"})
         */
        public function listAction($page)
        {
            // ...
        }
    
        /**
         * @Route("/blog/{slug}", name="blog_show")
         */
        public function showAction($slug)
        {
            // ...
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/routing.yml
    blog_list:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:list }
        requirements:
            page: '\d+'
    
    blog_show:
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- 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_list" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:list</default>
            <requirement key="page">\d+</requirement>
        </route>
    
        <!-- ... -->
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:list',
    ), array(
        'page' => '\d+'
    )));
    
    // ...
    
    return $collection;
    

\d+ - это регулярное выражение, которое подходит под набор цифр любой длины. Теперь:

URL Route Parameters
/blog/2 blog_list $page = 2
/blog/yay-routing blog_show $slug = yay-routing

Чтобы узнать о других ограничениях маршрутов, например, HTTP-методе, имени хоста и динамических выражениях, см. Ограничения маршрутов.

Устанавливаем значение по умолчанию для {placeholder}

В предыдущем примере, blog_list имеет путь /blog/{page}. Если пользователь зайдет на /blog/1, он подойдёт. Но если пользователь зайдет на /blog, то он не подойдёт. Как только вы добавите в маршрут {placeholder}, он должен иметь значение.

Так как же сделать так, чтобы blog_list подходил, когда пользователь заходит на /blog? Путем добавления значения по умолчанию:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    // 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/{page}", name="blog_list", requirements={"page": "\d+"})
         */
        public function listAction($page = 1)
        {
            // ...
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/routing.yml
    blog_list:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:list, page: 1 }
        requirements:
            page: '\d+'
    
    blog_show:
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- 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_list" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:list</default>
            <default key="page">1</default>
    
            <requirement key="page">\d+</requirement>
        </route>
    
        <!-- ... -->
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route(
        '/blog/{page}',
        array(
            '_controller' => 'AppBundle:Blog:list',
            'page'        => 1,
        ),
        array(
            'page' => '\d+'
        )
    ));
    
    // ...
    
    return $collection;
    

Теперь, когда пользователь заходит на /blog, маршрут blog_list будет подходить, а $page теперь по умолчанию будет иметь значение 1.

Пример продвинутой маршрутизации

Держа все это в уме, посмотрите на этот продвинутый пример:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // src/AppBundle/Controller/ArticleController.php
    
    // ...
    class ArticleController extends Controller
    {
        /**
         * @Route(
         *     "/articles/{_locale}/{year}/{slug}.{_format}",
         *     defaults={"_format": "html"},
         *     requirements={
         *         "_locale": "en|fr",
         *         "_format": "html|rss",
         *         "year": "\d+"
         *     }
         * )
         */
        public function showAction($_locale, $year, $slug)
        {
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/routing.yml
    article_show:
      path:     /articles/{_locale}/{year}/{slug}.{_format}
      defaults: { _controller: AppBundle:Article:show, _format: html }
      requirements:
          _locale:  en|fr
          _format:  html|rss
          year:     \d+
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- 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}/{slug}.{_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
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 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}/{slug}.{_format}', array(
            '_controller' => 'AppBundle:Article:show',
            '_format'     => 'html',
        ), array(
            '_locale' => 'en|fr',
            '_format' => 'html|rss',
            'year'    => '\d+',
        ))
    );
    
    return $collection;
    

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

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

Этот пример также использует особый параметр маршрутизации _format. При использовании этого параметра, соответствующее значение становится «форматом запроса» объекта Request.

В конечном счете, формат запроса используется для таких вещей, как установка Content-Type для ответа (например запрос формата json меняет Content-Type в application/json). Он также может быть использван в контроллере для отображения разных шаблонов для каждого значения _format. Параметр _format – это очень мощный способ отображать одинаковый контент в разных форматах.

В версиях Symfony предшествующих 3.0, формат запроса возможно заместить, добавив параметр запроса под именем _format (например, /foo/bar?_format=json). Использование этого поведения не только считается плохой практикой, но еще и усложнит обновление ваших приложений до Symfony 3.

Note

Иногда мы можете захотеть сделать некоторые части ваших маршрутов настраиваемыми глобально. Symfony предоставляет вам возможность сделать это путем ипользования параметров контейнера служб. Прочитайте об этом подробнее в разделе "Параметры контейнеров служб".

Caution

Имя заполнителя маршрута (placeholder) не может начинаться с цифры и не может иметь больше 32 символов.

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

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

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

Используется для установки идентификатора фрагманта, необязательной последней части URL, которая начинается с символа # и используется для идентификации части документа.

New in version 3.2: Параметр _fragment появился в Symfony 3.2.

_locale
Используется для установки локали запроса (узнать больше).

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

Если вы используете конфигурацию маршрута YAML, XML или PHP, тогда каждый маршрут должен иметь параметр _controller, который указывает, какой контроллер должен быть выполнен, если этот маршрут подходит. Этот параметр использует простой шаблон строки под названием логическое имя контроллера, который Symfony соотносит с конкретным PHP-методом и классом. Шаблон состоит из трёх частей, разделённых двоеточием:

bundle:controller:action

Например, если _controller имеет значение AppBundle:Blog:show, то это означает:

Пакет Класс контроллера Имя метода
AppBundle BlogController showAction()

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 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. Но, если вы следуете нескольким простым соглашениям, логическое имя будет более кратким и будет иметь большую гибкость.

Tip

Чтобы сослаться на действие, выполняемое в качестве метода __invoke() класса контроллера, вам не нужно указывать имя метода, вы можете ограничиться использованием полностью квалифицированным именем класса (например, AppBundle\Controller\BlogController).

Note

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

Загрузка маршрутов

Symfony загружает все маршруты в ваше приложение из единого файла конфигурации маршрутов app/config/routing.yml. Но из этого файла вы можете загрузить любой другой файл маршрутизации, который вам нужен. На самом деле, по умолчанию, Symfony загружает аннотацию конфигурации маршрута из вашего каталога AppBundle Controller/, и это то, как Symfony видит наши маршруты аннотаций:

  • YAML
    1
    2
    3
    4
    # app/config/routing.yml
    app:
        resource: "@AppBundle/Controller/"
        type:     annotation
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <!-- 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">
    
        <!-- атрибут "type" необходим для включения парсера аннотаций для этого ресурса -->
        <import resource="@AppBundle/Controller/" type="annotation"/>
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection(
        // второй аргумент - это тип, которй необходим для включения
        // парсера аннотаций для этого ресурса
        $loader->import("@AppBundle/Controller/", "annotation")
    );
    
    return $collection;
    

Чтобы узнать больше информации по загрузке маршрутов, включая то, как использовать префиксы в путях загруженных маршрутов, см. Маршрутизация: внешние ресурсы.

Генерация URL

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

Для генерации URL, вам нужно будет указать имя маршрута (например, blog_show) и любые метасимволы (например, slug = my-blog-post), используемые в пути для этого маршрута. С этой информацией, можно с легкостью сгенерировать любой URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class MainController extends Controller
{
    public function showAction($slug)
    {
        // ...

        // /blog/my-blog-post
        $url = $this->generateUrl(
            'blog_show',
            array('slug' => 'my-blog-post')
        );
    }
}

Note

Метод generateUrl() указанный в базовом классе Controller - это всего лишь сокращение для такого кода:

1
2
3
4
$url = $this->container->get('router')->generate(
    'blog_show',
    array('slug' => 'my-blog-post')
);

Генерирование URL со строкой запроса

Метод generate() использует массив значений параметров для генерирования URL. Но если вы укажете дополнительные параметры, то они будут добавлены в URL как параметры запроса:

1
2
3
4
5
$this->get('router')->generate('blog', array(
    'page' => 2,
    'category' => 'Symfony'
));
// /blog/2?category=Symfony

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

Чтобы сгенерировать URL в Twig, см. статью по шаблонам. Если вам также надо сгенерировать URL в JavaScript, см Создание URL в JavaScript.

Генерирование абсолютных URL

По умолчанию, маршрутизатор генерирует относительные URL (например,``/blog``). Чтобы сгенерировать абсолютный URL, укажите UrlGeneratorInterface::ABSOLUTE_URL в качестве третьего аргумента метода generateUrl():

1
2
3
4
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

$this->generateUrl('blog_show', array('slug' => 'my-blog-post'), UrlGeneratorInterface::ABSOLUTE_URL);
// http://www.example.com/blog/my-blog-post

Note

Хост, который используется при генерации абсолютного URL, автоматически определяется при помощи текущего объекта Request. При генерировании абсолютных URL вне web-контекста (например, в консольной команде), этот способ не работает. См. Как генерировать URL из консоли, чтобы научиться решать эту проблему.

Устранение проблем

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

# Контроллер "AppBundleControllerBlogController::showAction()" требует, чтобы вы предоставили значение для аргумента "$slug".

Controller "AppBundleControllerBlogController::showAction()" requires that you provide a value for the "$slug" argument.

Это происходит, когда ваш метод контроллера имеет аргумент (например, $slug):

1
2
3
4
public function showAction($slug)
{
    // ..
}

Но ваш путь маршрута не имеет метасимвола {slug} (например, он /blog/show). Добавьте {slug} к вашему пути маршрута: /blog/show/{slug}, либо определите значение по умолчанию для аргумента (например, $slug = null).

Некоторым обязательным параметрам не хватает ("slug") для генерирования URL для маршрута "blog_show".

Это означает, что вы пробуете создать URL маршруту blog_show, но вы не указываете значение slug (которое является обязательным, так как оно имеет {slug}) для метасимвола в пути маршрута. Чтобы исправить это, укажите значение slug при генерировани маршрута:

1
2
3
4
$this->generateUrl('blog_show', array('slug' => 'slug-value'));

// или в Twig
// {{ path('blog_show', {'slug': 'slug-value'}) }}

Заключение

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

Продолжайте!

Маршрутизация - готово! Теперь, исследуйте силу контроллеров.

Узнайте больше о маршрутизации

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.