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

Дата обновления перевода 2021-12-25

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

Когда ваше приложение получает запрос, оно вызывает действие контроллера action, чтобы сгенерировать ответ. Конфигурациия маршутизации определяет, какое действие выполнять для каждого входящего URL. Она также предоставляет другие полезные функции, вроде генерирования дружелюбных для SEO URL (например, /read/intro-to-symfony вместо index.php?article_id=57).

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

Маршруты могут быть сконфигурированы на YAML, XML, PHP или с использованием атрибутов или аннотаций. Все форматы предоставляют одинаковые функции и производительность, поэтому выбирайте то, что вам больше нравится. Symfony рекомендует атрибуты, так как это удобно - помещать маршрут и контроллер в одно место.

Создание маршрутов в виде атрибутов или аннотаций

В PHP 8, вы можете использовать нативные атрибуты для немедленной конфигурации маршрутов. В PHP 7, где атрибуты недоступны, вы можете использовать вместо этого аннотации, предоставленные библиотекой Аннотаций Doctrine.

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

1
$ composer require doctrine/annotations

5.2

Возможность использовать PHP-атрибуты для конфигурации маршрутов, была представлена в Symfony 5.2. Раньше, Аннотации Doctrine были единственным способом аннотировать действия контроллера конфигурацией маршрутизации.

Эта команда также создает следующий файл конфигурации:

1
2
3
4
5
6
7
8
# config/routes/annotations.yaml
controllers:
    resource: ../../src/Controller/
    type: annotation

kernel:
    resource: ../../src/Kernel.php
    type: annotation

Эта конфигурация сообщает Symfony, что нужно искать маршруты, определенные как аннотации в любом PHP-классе, хранящемся в каталоге src/Controller/.

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

  • Annotations
  • Attributes
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\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog_list")
     */
    public function list()
    {
        // ...
    }
}

Эта конфигурация определяет маршрут под названием blog_list, который совпадает, когда пользователь запрашивает URL /blog. Когда происходит совпадение, приложение выполняет метод list() класса BlogController.

Note

Строка запроса URL не рассматривается при сопоставлении маршрутов. В этом примере, URL вроде /blog?foo=bar и /blog?foo=bar&bar=foo будут так же совпадать с маршрутом blog_list.

Caution

Если вы определяете несколько PHP-классов в одном файле, Symfony загружает только маршруты первого класса, игнорируя все другие.

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

Создание маршрутов в файлах YAML, XML или PHP

Вместо определения маршрутов в классах контроллера, вы можете определять их в отдельном файле YAML, XML или PHP. Главное преимущество - они не будут требовать никаких дополнительных зависимостей. Главный недостаток - вам нужно работать с несколькими файлами при проверке маршрутизации какого-то действия контроллера.

Следующий пример показывает, как определять в YAML/XML/PHP маршрут под названием blog_list, который ассоциирует URL /blog с действием list() BlogController:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
# config/routes.yaml
blog_list:
    path: /blog
    # значение контроллера ммеет формат 'controller_class::method_name'
    controller: App\Controller\BlogController::list

    # еслм действие реализуется как метод __invoke() класса контроллера,
    # вы можете пропустить часть '::method_name':
    # controller: App\Controller\BlogController

5.1

Начиная с Symfony 5.1, по умолчанию Symfony загружает только маршруты, определенные в формате YAML. Если вы определяете маршруты в формате XML и/или PHP, обновите файл src/Kernel.php, чтобы добавить поддержку расширений файлов .xml и .php.

Совпадение HTTP-методов

По умолчанию, маршруты совпадают с любым глаголом HTTP (GET, POST, PUT, и др.). Используйте опцию methods, чтобы ограничить глаголы, на которые долшжен реагировать каждый маршрут:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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
// src/Controller/BlogApiController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogApiController extends AbstractController
{
    /**
     * @Route("/api/posts/{id}", methods={"GET","HEAD"})
     */
    public function show(int $id): Response
    {
        // ... вернуть JSON-ответ с постом
    }

    /**
     * @Route("/api/posts/{id}", methods={"PUT"})
     */
    public function edit(int $id): Response
    {
        // ... редактировать пост
    }
}

Tip

HTML-формы поддерживают только методы GET и POST. Если вы вызываете маршрут с другим методом из HTML-формы, добавьте скрытое поле под названием _method с методом для использования (например, <input type="hidden" name="_method" value="PUT"/>). Если вы создаете ваши формы с помощью Форм Symfony это делается за вас автоматически.

Совпадающие выражения

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

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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/DefaultController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route(
     *     "/contact",
     *     name="contact",
     *     condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
     * )
     *
     * выражения также могут включать в себя параметры конфигурации:
     * condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'"
     */
    public function contact(): Response
    {
        // ...
    }
}

Значение опции condition - это любое валдиное выражение ExpressionLanguage и может использовать любую из этих переменных, созданных Symfony:

context
Экземпляр RequestContext, который содержит наиболее фунламентальную информацию о сопоставляемом маршруте.
request
Объект Запроса Symfony, который представляет текущий запрос.

За кулисами. выражения компилируются в чистый PHP. Из-за этого, использование ключа condition не вызывает дополнительной нагрузки кроме времени, необходимого для выполнения низлежащего PHP.

Caution

Условия не берутся во внимание при генерировании URL (что объясняется позже в этой статье).

Отладка маршрутов

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

1
2
3
4
5
6
7
8
9
10
11
12
$ php bin/console debug:router

----------------  -------  -------  -----  --------------------------------------------
Имя               Метод    Схема    Хост   Путь
----------------  -------  -------  -----  --------------------------------------------
homepage          ANY      ANY      ANY    /
contact           GET      ANY      ANY    /contact
contact_process   POST     ANY      ANY    /contact
article_show      ANY      ANY      ANY    /articles/{_locale}/{year}/{title}.{_format}
blog              ANY      ANY      ANY    /blog/{page}
blog_show         ANY      ANY      ANY    /blog/{slug}
----------------  -------  -------  -----  --------------------------------------------

Передайте имя (или его часть) какого-то маршрута этому аргументу, чтобы отобразить детали маршрута:

1
2
3
4
5
6
7
8
9
10
11
$ php bin/console debug:router app_lucky_number

+-------------+---------------------------------------------------------+
| Свойство    | Значение                                                |
+-------------+---------------------------------------------------------+
| Route Name  | app_lucky_number                                        |
| Path        | /lucky/number/{max}                                     |
| ...         | ...                                                     |
| Options     | compiler_class: Symfony\Component\Routing\RouteCompiler |
|             | utf8: true                                              |
+-------------+---------------------------------------------------------+

Другая команда называется router:match и она показывает, какой маршрут будет совпадать с заданным URL. It's useful to find out why some URL is not executing the controller action that you expect:

1
2
3
$ php bin/console router:match /lucky/number/8

  [OK] Route "app_lucky_number" matches

Параметры маршрута

Предыдущие примеры определяют маршруты, где URL никогда не изменяется (например, /blog). Однако, часто определяют маршруты, где какая-то часть - переменная. Например, URL для отображения какого-то поста блога скорее всего будет включать в себя название или слаг (например, /blog/my-first-post или /blog/all-about-symfony).

В маршрутах Symfony, переменные части заключены в { ... } и должны иметь уникальное имя. Например, маршрут для отображения содержания поста блога, определяется как /blog/{slug}:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    // ...

    /**
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function show(string $slug): Response
    {
        // $slug будет равен динамической части URL
        // например, /blog/yay-routing, затем $slug='yay-routing'

        // ...
    }
}

Имя переменной части ({slug} в этом примере) используется для создания PHP-переменной, где хранится содержание этого маршрута и передается контроллеру. Если пользователь посещает URL /blog/my-first-post, Symfony выполняет метод show() в классе BlogController, и передает аргумент $slug = 'my-first-post' методу show().

Маршруты могут определять любое количество параметров, но каждый из них может быть использовать только единожды в каждом маршруте (например, /blog/posts-about-{category}/page/{pageNumber}).

Валидация параметров

Представьте, что ваше приложение имеет маршрут blog_show (URL: /blog/{slug}) и маршрут blog_list (URL: /blog/{page}). Учитывая, что параметры маршрута принимают любые значения, нет возможности дифферинциировать эти два маршрута.

Если пользователь запрашивает /blog/my-first-post, оба маршрута совпадут, и Symfony будет использовать маршрут, который был определен первым. Чтобы исправить это, добавьте некоторую валидацию к параметру {page}, используя опцию requirements:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
     */
    public function list(int $page): Response
    {
        // ...
    }

    /**
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function show(string $slug): Response
    {
        // ...
    }
}

Опция requirements определяет регулярные PHP-выражения, которым должны соответствовать параметры маршрута для того, чтобы совпадал весь маршрут. В этом примере, \d+ - это регулярное выражение, которое совпадает с однозначным числом любой длины. Теперь:

URL ??????? ?????????
/blog/2 blog_list $page = 2
/blog/my-first-post blog_show $slug = my-first-post

Tip

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

Tip

Параметры также поддерживают свойства PCRE Unicode, которые являются последовательностями экранирования, совпадаюшими с общими типами символов. Например, \p{Lu} совпадает с любым заглавным символом на любом языке, \p{Greek} совпадает с любым греческим символом и т.д.

Note

При использовании регулярных выражений в параметрах маршрута, вы можете установить опцию маршрута utf8 как true, чтобы сделать так, чтобы любой символ . совпадал с любым символом UTF-8, а не только с одним битом.

Если вы хотите, требования можно встроить в каждый параметр, используя синтаксис {parameter_name<requirements>}. Эта функция делает конфигурацию более компактной, но может уменьшить читаемость маршрута, если требования сложные:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog/{page<\d+>}", name="blog_list")
     */
    public function list(int $page): Response
    {
        // ...
    }
}

Необязательные параметры

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

Вы также можете сделать так, чтобы blog_list снова совпадал, когда пользователь посещает /blog, добавив значение по умолчанию к параметру {page}. При использовании аннотаций, значения по умолчанию определяются в аргумнтах действия контроллера. В других форматах конфигурации они определяются опцией defaults:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
     */
    public function list(int $page = 1): Response
    {
        // ...
    }
}

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

Caution

Вы можете иметь более одного необязательного параметра (например, /blog/{slug}/{page}), но все после необязательного параметра должно быть необязательно. Например, /{page}/blog - это валидный путь, но page всегда будет обязательным (т.е. /blog не будет совпадать с этим маршрутом).

Если вы хотите всегд включать какое-то значение по умолчанию в сгенерированном URL (например, для генерирования /blog/1 вместо /blog в предыдущем примере), добавьте символ ! перед именем параметра: /blog/{!page}

Как это происходит с требованиями, значения по умолчанию также могут быть встроены в каждый параметр, используя синтаксис {parameter_name?default_value}. Эта функция совместима со встроенными требованиями, поэтому вы можете встроить обе в один параметр:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog/{page<\d+>?1}", name="blog_list")
     */
    public function list(int $page): Response
    {
        // ...
    }
}

Tip

Чтобы дать значение по умолчанию null любому параметру, ничего не добавляйте после символа ? (например, /blog/{page?}). Если вы так сделаете, не забудьте обновить типы связанных аргументов контроллера, чтобы позволить передачу значений null (например, замените int $page на ?int $page)

Параметр приоритетности

5.1

Параметр priority был представлен в Symfony 5.1

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

  • Annotations
  • Attributes
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
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * Этот маршрут имеет жадный паттерн и определяется первым.
     *
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function show(string $slug)
    {
        // ...
    }

    /**
     * Этот маршрут не может быть сопоставлен без определения приоритета выше, чем 0.
     *
     * @Route("/blog/list", name="blog_list", priority=2)
     */
    public function list()
    {
        // ...
    }
}

Параметр приоритета ожидает целое значение. Маршруты с более высоким приоритетом сортируются до маршрутов с более низким приоритетом. Значение по умолчанию, если параметр не определен, - 0.

Конверсия параметров

Распространенной потребностью маршрутизации является конверсия значения, хранящегося в некотором параметре (например, целое число, действующее, как ID пользователя), в другое значение (например, объект, представляющий пользователя). Эта функция называется "param converter".

Чтобы добавить поддержку "param converters", нам нужен SensioFrameworkExtraBundle:

1
$ composer require sensio/framework-extra-bundle

Теперь, оставьте предыдущую конфигурацию маршрута, но измените аргументы действия контроллера. Вместо string $slug, добавьте BlogPost $post:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/BlogController.php
namespace App\Controller;

use App\Entity\BlogPost;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    // ...

    /**
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function show(BlogPost $post): Response
    {
        // $post это объект, чей слаг сооветствует параметру маршрутизации

        // ...
    }
}

Если ваши аргументы контроллера включают в себя подсказки для объектов (BlogPost в этом случае), "param converter" делает запрос в базу данных, чтобы найти объект, использующий параметры запроса (slug в этом случае). Если объект не найден, Symfony автоматически генерирует ответ 404.

Прочтите полную документацию param converter, чтобы узнать о преобразователях, предоставленных Symfony, и о том, как их сконфигурировать.

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

В дополнение к вашим собственным параметрам, маршруты могут иметь любые следующие параметры, созданные Symfony:

_controller
Этот параметр используется для определения того, какой контроллер и действие выполняется при совпадении маршрута.
_format
Совпавшее значение используется для установки "request format" объекта Request. Это используется для таких вещей, как установка Content-Type ответа (например, формат json переводится в Content-Type для application/json).
_fragment
Используется для установки идентификатора фрагмента, что является последней необязательной частью URL, которая начинается с символа # и используется для идентификации части документа.
_locale
Используется для установки локали в запросе.

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

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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/ArticleController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ArticleController extends AbstractController
{
    /**
     * @Route(
     *     "/articles/{_locale}/search.{_format}",
     *     locale="en",
     *     format="html",
     *     requirements={
     *         "_locale": "en|fr",
     *         "_format": "html|xml",
     *     }
     * )
     */
    public function search(): Response
    {
    }
}

Дополнительные параметры

В опции маршрута defaults вы можете по желанию определить параметры, не включенные в конфигурацию маршрута. Это полезно для передачи дополнительных аргументов контроллерам маршрутов:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog/{page}", name="blog_index", defaults={"page": 1, "title": "Hello world!"})
     */
    public function index(int $page, string $title): Response
    {
        // ...
    }
}

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

Параметры маршрута могут содержать любые значения, кроме символа слеша /, потому что этот символ используется для разделения разных частей URL. Например, если значение token в маршруте /share/{token} содержит символ /, этот маршруте не будет совпадать.

Возможным решением будет изменение требований параметра, чтобы они были более вольными:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/share/{token}", name="share", requirements={"token"=".+"})
     */
    public function share($token): Response
    {
        // ...
    }
}

Note

Если маршрут определяет несколько параметров, и вы применяете это вольное регулярное выражение ко всем, вы можете получить неожиданные результаты. Например, если определение маршрута - /share/{path}/{token} и как path, так и token принимают /. затем token будет получать только последний путь и остаток совпадения соотносится с path.

Note

Если маршрут имеет специальный параметр {_format}, вам не стоит использовать требование .+ для параметров, позволяющих слеши. Например, если паттерн - /share/{token}.{_format}, а {token} позволяет любые символы, URL /share/foo/bar.json будет рассматривать foo/bar.json как токен, и формат будет пустым. Это можно решить заменив требование .+ на [^.]+, чтобы разрешить любые символы, кроме точек.

Группы и префиксы маршрутов

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

При определении маршрутов в виде атрибутов или аннотаций, поместите общую конфигурацию в атрибут #[Route] (или аннотацию @Route) класса контроллера. В другим форматах маршрутизации, определите общую конфигурацию, используя опции при импорте маршрутов.

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/blog", requirements={"_locale": "en|es|fr"}, name="blog_")
 */
class BlogController extends AbstractController
{
    /**
     * @Route("/{_locale}", name="index")
     */
    public function index(): Response
    {
        // ...
    }

    /**
     * @Route("/{_locale}/posts/{slug}", name="show")
     */
    public function show(Post $post): Response
    {
        // ...
    }
}

В этом примере, маршрут действия index() будет назван blog_index и его URL будет /blog/{_locale}. Маршрут действия show() будет назван blog_show и его URL будет /blog/{_locale}/posts/{slug}. Оба маршрута также будут валидировать, что параметр _locale совпадает с регулярным выражением, определенным в аннотации класса.

Note

Если любой из маршрутов с префиксом определяет пустой путь, Symfony добавляет к нему замыкающий слеш. В предыдущем примере, пустой путь с префиксом /blog, приведет к URL /blog/. Если вы хотите избежать такого поведения, установите опцию trailing_slash_on_root как false (эта опция недоступна при использовании атрибутов или аннотаций PHP):

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/routes/annotations.yaml
controllers:
    resource: '../../src/Controller/'
    type:     annotation
    prefix:   '/blog'
    trailing_slash_on_root: false
    # ...

See also

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

Получение имени и параметров маршрута

Объект Request, созданный Symfony хранит всю конфигурацию маршрута (такую как имя и параметры) в "атрибутах запроса". Вы можете получить эту информацию в контроллере через объект Request:

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\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog_list")
     */
    public function list(Request $request): Response
    {
        $routeName = $request->attributes->get('_route');
        $routeParameters = $request->attributes->get('_route_params');

        // используйте это, чтобы получить все доступные атрибуты (не только маршрутизации):
        $allAttributes = $request->attributes->all();

        // ...
    }
}

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

1
2
3
4
5
{% set route_name = app.request.attributes.get('_route') %}
{% set route_parameters = app.request.attributes.get('_route_params') %}

{# use this to get all the available attributes (not only routing ones) #}
{% set all_attributes = app.request.attributes.all %}

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

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

Отображение шаблона прямо из маршрута

Прочтите раздел об отображении шаблона из маршрута в главной статье о шаблонах Symfony.

Перенапрвоение на URL и маршруты прямо из маршрута

Используйте RedirectController, чтобы перенаправить на другие маршруты и URL:

  • YAML
  • XML
  • PHP
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
# config/routes.yaml
doc_shortcut:
    path: /doc
    controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
    defaults:
        route: 'doc_page'
        # по желанию вы можете определить какие-то аргументы, переданные маршруту
        page: 'index'
        version: 'current'
        # перенаправляния временные по умолчанию (код 302), но вы можете сделать их постоянными (код 301)
        permanent: true
        # добавьтье это, чтобы сохранить изначальные параметры строки запроса при перенаправлении
        keepQueryParams: true
        # добавьте это, чтобы оставить метод HTTP при перенаправлении. Статус перенаправления изменяется
        # * для временных перенаправлений, используется статус-код 307 вместо 302
        # * для постоянных перенаправлений, используется статус-код 307 вместо 301
        keepRequestMethod: true

legacy_doc:
    path: /legacy/doc
    controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
    defaults:
        # это значение может быть абсолютным путем или URL
        path: 'https://legacy.example.com/doc'
        permanent: true

Tip

Symfony также предоставляет некоторые утилиты для перенаправлений внутри контроллеров

Перенаправление URL с замыкающими слешами

Исторически, URL следовали соглашению UNIX о добавлении замыкающих слешей для каталогов (например, https://example.com/foo/) и их удаления для ссылания на файлы (https://example.com/foo). Хотя обслуживание разного содержания для обоих URL - это ок, сейчас чащей всего оба URL будут рассматриваться как один и перенаправление меджу ними.

Symfony следует этой логике для перенаправления между URL с и без замыкающего слеша (но только для запросов GET и HEAD):

??????? URL ???? ??????????? URL /foo ???? ??????????? URL /foo/
/foo C???????? (??????-????? 200) ?????? ??????????????? 301 ?? /foo
/foo/ ?????? ??????????????? 301 ?? /foo C???????? (??????-????? 200)

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

Маршруты могут конфигурироать опцию host, чтобы требовать, чтобы HTTP-хост входящих запросов совпадал с некоторым конкретным значением. В следующем примере, оба маршрута совпадают с одним путем (/), но один из них отвечает только на определенное имя хоста:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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
// src/Controller/MainController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="mobile_homepage", host="m.example.com")
     */
    public function mobileHomepage(): Response
    {
        // ...
    }

    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        // ...
    }
}

Значение опции host может иметь параметры (что полезно в мультитенатных приложениях) и эти параметры могут быть тоже валидированы с помощью requirements:

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
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/MainController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route(
     *     "/",
     *     name="mobile_homepage",
     *     host="{subdomain}.example.com",
     *     defaults={"subdomain"="m"},
     *     requirements={"subdomain"="m|mobile"}
     * )
     */
    public function mobileHomepage(): Response
    {
        // ...
    }

    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        // ...
    }
}

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

Tip

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

Note

При использовании маршрутизации субкаталогов, вы должны устанавливать HTTP-заголовки Host в функциональных тестах, иначе маршруты не будут совпадать:

1
2
3
4
5
6
7
8
9
$crawler = $client->request(
    'GET',
    '/',
    [],
    [],
    ['HTTP_HOST' => 'm.example.com']
    // или получить значение из какого-то параметра контейнера:
    // ['HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain')]
);

Tip

Вы также можете использовать встроенный формат по умолчаниюи и требования в опции host: {subdomain<m|mobile>?m}.example.com

5.2

Поддержка значений встроенных параметров по умолчанию в хостах была представлена в Symfony 5.2. До Symfony 5.2, они поддерживались только в пути.

Локализованные маршруты (i18n)

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

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/CompanyController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class CompanyController extends AbstractController
{
    /**
     * @Route({
     *     "en": "/about-us",
     *     "nl": "/over-ons"
     * }, name="about_us")
     */
    public function about(): Response
    {
        // ...
    }
}

Note

При использовании PHP-атрибутов для локализованных маршрутов, вам нужно использовать параметр по имени path, чтобы указать массив путей.

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

Tip

Когда приложение использует полные локали "язык + территория" (например, fr_FR, fr_BE), если URL одинаковы во всех связанных локалях, маршруты могут спользовать только часть языка (например, fr), чтобы избежать повторения одних и тех же URL.

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

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/routes/annotations.yaml
controllers:
    resource: '../../src/Controller/'
    type: annotation
    prefix:
        en: '' # don't prefix URLs for English, the default locale
        nl: '/nl'

Другое распространенное требование - размещать веб-сайт на разных доменах в соответствии с локалью. Это может быть сделано путем определения разных хостов для каждой локали.

5.1

Возможность определять массив хостов была представлена в Symfony 5.1.

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/routes/annotations.yaml
controllers:
    resource: '../../src/Controller/'
    type: annotation
    host:
        en: 'https://www.example.com'
        nl: 'https://www.example.nl'

Маршруты без состояния

5.1

Опция stateless была представлена в Symfony 5.1.

Иногда, когда HTTP-ответ должен быть кеширован, важно убедиться, что это может произойти. Однако, каждый разm когда во время запроса начинается сессия, Symfony превращает ответ в частный некешируемый ответ.

Для деталей, см. HTTP кеширование.

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

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Controller/MainController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="homepage", stateless=true)
     */
    public function homepage()
    {
        // ...
    }
}

Теперь, если сессия используется, приложение сообщит о ней, основываясь на вашем параметре kernel.debug:

Это поможет вам понять, и, надеемся, исправить неожиданное поведение вашего приложения.

Генерирование URL

Системы маршрутизации двусторонни: 1) они ассоциируют URL с контроллерами (как объясняется в предыдущих разделах); 2) они генерируют URL для заданного маршрута. Генерирование URL из маршрутов позволяет вам не писать значения <a href="..."> вручную в ваших HTML-шаблонах. Также, если URL какого-то маршрута изменяется, вы только должны обновить конфигурацию маршрута и все ссылки будут обновлены.

Чтобы сгенерировать URL, вам нужно указать имя маршрута (например, blog_show) и знчения параметров, определенных маршрутами (например, slug = my-blog-post).

По этой причина каждый маршрут имеет внутреннее имя, которое должно быть уникально в приложении. Если вы не установите имя маршрута ясно с опцией name, Symfony генерирует автоматическое имя, основываясь на контроллере и действии.

Генерирование URL в контроллерах

Если ваш контроллер расширяется из AbstractController, используйте помощник generateUrl():

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
32
33
34
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog_list")
     */
    public function list(): Response
    {
        // сгенерируйте URL без аргументов маршрута
        $signUpPage = $this->generateUrl('sign_up');

        // сгенарируйте URL с аргументами марашрута
        $userProfilePage = $this->generateUrl('user_profile', [
            'username' => $user->getUserIdentifier(),
        ]);

        // сгенерированные URL являются "абсолютными путями" по умолчанию. Передайте третий необязательный
        // аргумент, чтобы сгенерировать другие URL (например, "абсолютный URL")
        $signUpPage = $this->generateUrl('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);

        // когда маршрут локализован, Symfony по умолчанию использует текущую локаль запроса
        // передайте другое знаениче '_locale', если вы хотите установить локаль ясно
        $signUpPageInDutch = $this->generateUrl('sign_up', ['_locale' => 'nl']);

        // ...
    }
}

Note

Если вы передаетет методу generateUrl() какие-то пааметры, которые не являются частью определения маршрута, они включаются в сгенерированный URL как строка запроса:

1
2
3
$this->generateUrl('blog', ['page' => 2, 'category' => 'Symfony']);
// маршрут 'blog' определяет только параметр 'page'; сгенерированный URL:
// /blog/2?category=Symfony

Caution

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

1
$this->generateUrl('blog', ['uuid' => (string) $entity->getUuid()]);

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

Генерирование URL в сервисах

Внедрите сервис Symfony router в ваши собственные сервисы и используйте его метод generate(). При использовании автомонтирования сервисов, вам понадобится только добавить аргумент в конструктор сервиса и типизировать его классом UrlGeneratorInterface:

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
32
33
34
35
// src/Service/SomeService.php
namespace App\Service;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class SomeService
{
    private $router;

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

    public function someMethod()
    {
        // ...

        // сгенерируйте URL без аргументов маршрута
        $signUpPage = $this->router->generate('sign_up');

        // сгенерируйте URL с аргументами маршрута
        $userProfilePage = $this->router->generate('user_profile', [
            'username' => $user->getUserIdentifier(),
        ]);

        // сгенерированные URL являются "абсолютными путями" по умолчанию. Передайте третий необязательный
        // аргумент, чтобы сгенерировать другие URL (например, "абсолютный URL")
        $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);

        // когда маршрут локализован, Symfony по умолчанию использует текущую локаль запроса
        // передайте другое знаениче '_locale', если вы хотите установить локаль ясно
        $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']);
    }
}

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

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

Генерирование URL в JavaScript

Если ваш код JavaScript включен в шаблон Twig, вы можете использовать функции Twig path() и url(), чтобы сгенрировать URL и сохранить их в переменных JavaScript. Фильтр escape() необходим для экранирования любых небезопасных для JavaScript значений:

1
2
3
<script>
    const route = "{{ path('blog_show', {slug: 'my-blog-post'})|escape('js') }}";
</script>

Если вам нужно сгенерировать URL динамически или если вы используете чистый код JavaScript, это решение не работает. D таких случаях рассмотрите использование FOSJsRoutingBundle.

Генерирование URL в командах

Генерирование URL в командах работает так же, как генерирование URL в сервисах. Единственная разница в том, что команды не выполняются в HTTP-контексте. Следовательно, если вы генерируете абсолютные URL, вы получите http://localhost/ в качестве имени хоста вместо вашего реального имени хоста.

Решением будет сконфигурировать опцию default_uri, чтобы определить "контекст запроса", используемы командами, когда они генерируют URL:

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/packages/routing.yaml
framework:
    router:
        # ...
        default_uri: 'https://example.org/my/path/'

5.1

Опция default_uri была представлена в Symfony 5.1.

Теперь вы получите ожидаемые результаты при генерировании URL в ваших командах:

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
32
33
34
35
36
37
38
39
40
41
42
// src/Command/SomeCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
// ...

class SomeCommand extends Command
{
    private $router;

    public function __construct(RouterInterface $router)
    {
        parent::__construct();

        $this->router = $router;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // сгенерируйте URL без аргументов маршрута
        $signUpPage = $this->router->generate('sign_up');

        // сгенерируйте URL с аргументами маршрута
        $userProfilePage = $this->router->generate('user_profile', [
            'username' => $user->getUserIdentifier(),
        ]);

        // сгенерированные URL являются "абсолютными путями" по умолчанию. Передайте третий необязательный
        // аргумент, чтобы сгенерировать другие URL (например, "абсолютный URL")
        $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);

        // когда маршрут локализован, Symfony по умолчанию использует текущую локаль запроса
        // передайте другое знаениче '_locale', если вы хотите установить локаль ясно
        $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']);

        // ...
    }
}

Note

По умолчанию, URL, сгенерированные для веб-ресурсов, используют одно и то же значение default_uri, но вы можете изменить это с помощью параметров контейнера asset.request_context.base_path и asset.request_context.secure.

Проверка существования маршрута

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

Вместо этого, попоробуйте сгенрировать URL и поймать RouteNotFoundException, вызванное, когда маршрут не существует:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Routing\Exception\RouteNotFoundException;

// ...

try {
    $url = $this->router->generate($routeName, $routeParameters);
} catch (RouteNotFoundException $e) {
    // маршрут не определен...
}

Форсирование HTTPS в сгенерированных URL

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

  • YAML
  • XML
  • PHP
1
2
3
4
# config/services.yaml
parameters:
    router.request_context.scheme: 'https'
    asset.request_context.secure: true

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

  • Annotations
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Controller/SecurityController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="login", schemes={"https"})
     */
    public function login(): Response
    {
        // ...
    }
}

URL, сгенерированный для маршрута login всегда будет использовать HTTPS. Это означает, что при использовании функции Twig path() для генерирования URL, вы можете получить абсолютный URL вместо относительного, если HTTP-схема изнчального запроса отличается от схемы, используемой маршрутом:

1
2
3
4
5
6
{# если текущая схема - HTTPS, генерирует относительный URL: /login #}
{{ path('login') }}

{# если текущая схема - HTTP, генерирует абсолютный URL для изменения
   схемы: https://example.com/login #}
{{ path('login') }}

Требование схемы также форсируется для входящих запросов. Если вы попробуете получить дотуп к URL /login с HTTP, вы автоматически будете перенаправлены на тот же URL, но с HTTPS-схемой.

Если вы хотите заставить группу маршрутов использовать HTTPS, вы можете определить схему по умолчанию при их импортировании. Следующий пример форсирует HTTPS на все маршруты, определенные как аннотации:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
# config/routes/annotations.yaml
controllers:
    resource: '../../src/Controller/'
    type: annotation
    defaults:
        schemes: [https]

Note

Компонент Безопасность предоставляет другой способ форсирования HTTP или HTTPS через настройку requires_channel.

Отладка проблем

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

Controller "App\Controller\BlogController::show()" требует, чтобы вы предоставили значение для аргумента "$slug".

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

1
2
3
4
public function show(string $slug): Response
{
    // ...
}

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

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

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

1
$this->generateUrl('blog_show', ['slug' => 'slug-value']);

или, в Twig:

1
{{ path('blog_show', {slug: 'slug-value'}) }}