SymfonyJuniorTechnical

Как работает маршрутизация (routing) в Symfony и какими способами можно определять маршруты?

Маршруты в Symfony определяются через PHP-атрибуты #[Route] (рекомендуется), YAML или PHP-файл конфигурации; роутер сопоставляет URL с контроллером по пути, HTTP-методу и requirements-паттернам, а URL генерируется через generateUrl() или Twig-функцию path().

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

Роутинг — процесс сопоставления входящего HTTP-запроса с конкретным контроллером и его методом. Symfony использует компонент Routing, который работает через RouterListener на событии kernel.request.

Способы определения маршрутов

1. PHP-атрибуты (рекомендуется с Symfony 6)

<?php
namespace App\Controller;

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

#[Route('/blog', name: 'blog_')]
class BlogController extends AbstractController
{
    // GET /blog/
    #[Route('/', name: 'index')]
    public function index(): Response
    {
        return $this->render('blog/index.html.twig');
    }

    // GET /blog/42 или /blog/my-post — параметр с типом и дефолтом
    #[Route('/{slug}', name: 'show', requirements: ['slug' => '[a-z0-9-]+'])]
    public function show(string $slug): Response
    {
        return $this->render('blog/show.html.twig', ['slug' => $slug]);
    }

    // POST /blog/ — ограничение по методу
    #[Route('/', name: 'create', methods: ['POST'])]
    public function create(): Response
    {
        // ...
        return $this->redirectToRoute('blog_index');
    }

    // /blog/{id} где id = целое число
    #[Route('/{id}', name: 'delete', requirements: ['id' => '\d+'], methods: ['DELETE'])]
    public function delete(int $id): Response
    {
        return new Response(null, Response::HTTP_NO_CONTENT);
    }
}

2. YAML-конфигурация

# config/routes.yaml
blog_index:
    path: /blog/
    controller: App\Controller\BlogController::index
    methods: [GET]

blog_show:
    path: /blog/{slug}
    controller: App\Controller\BlogController::show
    requirements:
        slug: '[a-z0-9-]+'
    defaults:
        slug: 'latest'

# Импорт маршрутов из контроллеров
blog:
    resource: '../src/Controller/BlogController.php'
    type: attribute
    prefix: /blog

3. PHP-файл (routes.php)

<?php
// config/routes.php
use App\Controller\BlogController;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return function (RoutingConfigurator $routes): void {
    $routes->add('blog_index', '/blog/')
        ->controller([BlogController::class, 'index'])
        ->methods(['GET']);

    $routes->add('blog_show', '/blog/{slug}')
        ->controller([BlogController::class, 'show'])
        ->requirements(['slug' => '[a-z0-9-]+'])
        ->defaults(['slug' => 'latest']);
};

Генерация URL

<?php
// В контроллере
$url = $this->generateUrl('blog_show', ['slug' => 'hello-world']);
// Результат: /blog/hello-world

// Абсолютный URL
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$absoluteUrl = $this->generateUrl('blog_show', ['slug' => 'hello'], UrlGeneratorInterface::ABSOLUTE_URL);
// Результат: https://example.com/blog/hello
// В Twig-шаблоне
<a href="{{ path('blog_show', {slug: 'hello-world'}) }}">Читать</a>
<a href="{{ url('blog_show', {slug: 'hello-world'}) }}">Абсолютная ссылка</a>

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

# Список всех маршрутов
php bin/console debug:router

# Детали конкретного маршрута
php bin/console debug:router blog_show

# Проверить, какой маршрут соответствует URL
php bin/console router:match /blog/hello-world

Подводные камни

  • Порядок маршрутов имеет значение: первый подходящий маршрут побеждает. Маршрут /blog/new должен быть выше /blog/{slug}, иначе «new» попадёт в параметр slug.
  • Требования (requirements) для параметров — это регулярные выражения; забытые границы \d+ совпадут с «123abc» частично — используйте \d+ только если Symfony сам добавляет ^ и $.
  • При импорте маршрутов из аннотаций/атрибутов через resource с указанием директории кэш компилируется один раз — после добавления нового контроллера нужен cache:clear в production.
  • Метод generateUrl() бросает исключение если маршрут не найден по имени или не хватает обязательных параметров — всегда проверяйте имена маршрутов при переименовании.
  • Параметры маршрута с одинаковым именем в родительском #[Route] и дочернем #[Route] вызывают конфликт — не дублируйте параметры в префиксах контроллера.
  • Маршруты с методом DELETE/PUT недоступны из браузерных форм HTML; используйте скрытое поле _method и middleware HttpMethodOverride или делайте это через JavaScript fetch.

Common mistakes

  • Сводить routing к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет routing через конкретную точку lifecycle в Symfony.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics