Prefixes, Groups and Debugging
Put #[Route] on the class to prefix a group of routes, and use debug:router to see what the router really has.
#[Route('/articles', name: 'app_article_')] on the class; php bin/console debug:routerRoutes in one controller usually share a path and a naming scheme. A #[Route] attribute on the class supplies both. Its path is prefixed to every method's path, and its name is prefixed to every method's route name. A class-level requirements array applies to all of them too.
The concatenation is literal. Class name: 'app_article_' plus method name: 'show' gives app_article_show; drop the trailing underscore and you get app_articleshow. A method whose own #[Route] has an empty path matches the prefix itself.
Groups in YAML
The same grouping exists one level up. config/routes.yaml imports your controllers, and that import can carry prefix, name_prefix and requirements that apply to everything imported. This is how a whole application moves under /api without editing a single attribute.
Locale prefixes
A route's path can be an array keyed by locale, giving a different URL per language for one controller. Generating that URL uses the one route name and the current locale, or an explicit _locale parameter.
Debugging
Prefixes make it easy to be wrong about the final path. php bin/console debug:router lists every route with its methods and path; adding a route name shows that route alone. php bin/console router:match /articles/7 works the other way, reporting which route a URL hits. Check these before changing code.
Example
<?php
// src/Controller/ArticleController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// Class-level attribute: path prefix and name prefix for every
// method below. The trailing underscore in the name is required.
#[Route('/articles', name: 'app_article_')]
final class ArticleController extends AbstractController
{
// Empty path: matches the prefix itself, /articles.
// Full route name: app_article_index
#[Route('', name: 'index', methods: ['GET'])]
public function index(): Response
{
return new Response('<h1>Articles</h1>');
}
// Path /articles/new, name app_article_new
#[Route('/new', name: 'new', priority: 1, methods: ['GET'])]
public function new(): Response
{
return new Response('<h1>New article</h1>');
}
// Path /articles/{id}, name app_article_show
#[Route('/{id<\d+>}', name: 'show', methods: ['GET'])]
public function show(int $id): Response
{
return new Response(sprintf('Article %d', $id));
}
}
When to use it
- Give every route in a controller the same /articles prefix and app_article_ name prefix.
- Mount an existing set of controllers under /api without editing their attributes.
- Find out which route and controller a failing URL actually matches.
More examples
Seeing the result of a prefix
`debug:router` prints the routes as the router holds them, after every prefix has been applied, so it settles arguments about what a path really is. `--show-controllers` adds the class and method that each route runs, which is how you find the code behind an unfamiliar URL. `router:match` reports the matching route for a path, or tells you none matched.
php bin/console debug:router --show-controllers
# -------------------- -------- -------- ------ -------------------
# Name Method Scheme Host Path
# -------------------- -------- -------- ------ -------------------
# app_article_index GET ANY ANY /articles
# app_article_new GET ANY ANY /articles/new
# app_article_show GET ANY ANY /articles/{id}
# -------------------- -------- -------- ------ -------------------
# One route in detail, including its requirements and defaults:
php bin/console debug:router app_article_show
# Which route answers a given URL:
php bin/console router:match /articles/7
Prefixing an import in YAML
`prefix` and `name_prefix` on an import are the file-level equivalent of a class-level `#[Route]`. This is how a versioned or namespaced API gets its path segment in one place, and how a bundle's routes are mounted under a path you choose rather than the one its author chose.
# config/routes.yaml
# The default import that reads #[Route] attributes from src/Controller/.
controllers:
resource: routing.controllers
# A second import of the same kind can carry a prefix. Everything
# it loads gains /api on the path and api_ on the route name,
# without any attribute changing.
api_controllers:
resource: routing.controllers
prefix: /api
name_prefix: api_
One controller, a different URL per locale
Both `GET /about-us` and `GET /over-ons` return 200 from this one method, and generating `app_about` produces the URL for the current locale unless you pass `_locale` explicitly. Reach for this when the URLs themselves should be translated rather than carrying a `/en/` segment.
<?php
// src/Controller/AboutController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class AboutController extends AbstractController
{
// path as an array: one URL per locale, one route name.
// The entry without a key is the fallback for other locales.
#[Route(path: [
'en' => '/about-us',
'nl' => '/over-ons',
'/about-us',
], name: 'app_about', methods: ['GET'])]
public function about(): Response
{
// When a localized route matches, that locale is used for
// the rest of the request.
return new Response('<h1>About</h1>');
}
}
Discussion