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

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

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

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

Для начала, установите пакет аннотаций:

1
$ composer require annotations

Маршрут - это карта от пути URL к контроллеру. Представьте, что вы хотите один маршрут, точно совпадающий с /blog, а второй - более динамический, который может совпадать с любым 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/Controller/BlogController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\Routing\Annotation\Route;
    
    class BlogController extends Controller
    {
        /**
         * Matches /blog exactly
         *
         * @Route("/blog", name="blog_list")
         */
        public function list()
        {
            // ...
        }
    
        /**
         * Matches /blog/*
         *
         * @Route("/blog/{slug}", name="blog_show")
         */
        public function show($slug)
        {
            // $slug будет равняться динамической части URL
            // e.g. at /blog/yay-routing, then $slug='yay-routing'
    
            // ...
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # config/routes.yaml
    blog_list:
        path:     /blog
        controller: App\Controller\BlogController::list
    
    blog_show:
        path:     /blog/{slug}
        controller: App\Controller\BlogController::show
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- config/routes.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" controller="App\Controller\BlogController::list" path="/blog" >
            <!-- settings -->
        </route>
    
        <route id="blog_show" controller="App\Controller\BlogController::show" path="/blog/{slug}">
            <!-- settings -->
        </route>
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // config/routes.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    use App\Controller\BlogController;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route('/blog', array(
        '_controller' => [BlogController::class, 'list']
    )));
    $collection->add('blog_show', new Route('/blog/{slug}', array(
        '_controller' => [BlogController::class, 'show']
    )));
    
    return $collection;
    

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

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

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

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

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

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

Представьте, что путь 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/Controller/BlogController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\Routing\Annotation\Route;
    
    class BlogController extends Controller
    {
        /**
         * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
         */
        public function list($page)
        {
            // ...
        }
    
        /**
         * @Route("/blog/{slug}", name="blog_show")
         */
        public function show($slug)
        {
            // ...
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # config/routes.yaml
    blog_list:
        path:      /blog/{page}
        controller: App\Controller\BlogController::list
        requirements:
            page: '\d+'
    
    blog_show:
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/routes.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}" controller="App\Controller\BlogController::list">
            <requirement key="page">\d+</requirement>
        </route>
    
        <!-- ... -->
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // config/routes.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    use App\Controller\BlogController;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route('/blog/{page}', array(
        '_controller' => [BlogController::class, '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/Controller/BlogController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\Routing\Annotation\Route;
    
    class BlogController extends Controller
    {
        /**
         * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
         */
        public function list($page = 1)
        {
            // ...
        }
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # config/routes.yaml
    blog_list:
        path:      /blog/{page}
        controller: App\Controller\BlogController::list
        defaults:
            page: 1
        requirements:
            page: '\d+'
    
    blog_show:
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- config/routes.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}" controller="App\Controller\BlogController::list">
            <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
    20
    // config/routes.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    use App\Controller\BlogController;
    
    $collection = new RouteCollection();
    $collection->add('blog_list', new Route(
        '/blog/{page}',
        array(
            '_controller' => [BlogController::class, 'list'],
            'page'        => 1,
        ),
        array(
            'page' => '\d+'
        )
    ));
    
    // ...
    
    return $collection;
    

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

Список всех ваших маршрутов

По мере роста вашего приложения, у вас в итоге появится много маршрутов! Чтобы увидеть их все, выполните:

1
$ php bin/console debug:router
1
2
3
4
5
6
------------------------------ -------- -------------------------------------
 Имя                           Метод    Путь
------------------------------ -------- -------------------------------------
 app_lucky_number              ANY    /lucky/number/{max}
 ...
------------------------------ -------- -------------------------------------

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

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

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // src/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 show($_locale, $year, $slug)
        {
        }
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # config/routes.yaml
    article_show:
      path:     /articles/{_locale}/{year}/{slug}.{_format}
      controller: App\Controller\ArticleController::show
      defaults:
          _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
    <!-- config/routes.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}"
            controller="App\Controller\ArticleController::show">
    
            <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
    19
    // config/routes.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    use App\Controller\ArticleController;
    
    $collection = new RouteCollection();
    $collection->add(
        'article_show',
        new Route('/articles/{_locale}/{year}/{slug}.{_format}', array(
            '_controller' => [ArticleController::class, '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).

Note

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

Caution

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

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

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

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

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

Значене controller в ваших маршрутах имеет очень простой формат CONTROLLER_CLASS::METHOD. Если ваш контроллер зарегистрирован, как сервис, то вы также можете использовать только одно двоеточие-разделитель (например, service_name:index).

Tip

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

Генерирование 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 show($slug)
    {
        // ...

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

Если вам нужно сгенерировать URL из сервиса, типизируйте сервис UrlGeneratorInterface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Service/SomeService.php

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class SomeService
{
    private $router;

    public function __construct(UrlGeneratorInterface $router)
    {
        $this->router = $router;
    }

    public function someMethod()
    {
        $url = $this->router->generate(
            'blog_show',
            array('slug' => 'my-blog-post')
        );
        // ...
    }
}

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

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

1
2
3
4
5
$this->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 show($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'}) }}

Перевод маршрутов

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

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

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

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

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