Requirements and Priority
Constrain a placeholder with a regular expression, and use priority when two routes could match the same URL.
#[Route('/articles/{id<\d+>}', name: 'app_article_show', priority: 0)]A placeholder matches any segment by default. That is too loose. /articles/{id} will happily match /articles/banana and pass banana to a method expecting a number. A requirement is a regular expression that a placeholder must satisfy before the route matches at all. If it fails, the router moves on to the next route, and if nothing matches you get a 404.
Shorthand and longhand
The shorthand puts the pattern inside the braces, between angle brackets: {id<\d+>}. The longhand passes a requirements array keyed by placeholder name: requirements: ['id' => '\d+']. They compile to the same route. Combined with a default, the full inline form is {name<pattern>?default}.
Why /{id} and /new collide
With /articles/{id} and /articles/new both declared, /articles/new matches whichever was loaded first. Within one class, methods load top to bottom; across classes the order depends on how files are read, so it is not something to depend on.
There are two fixes. The better one is a requirement: {id<\d+>} cannot match new, so the ambiguity disappears. The other is priority, an integer on the #[Route] attribute, default 0. Higher priority is tried first, so priority: 1 on the /articles/new route wins regardless of load order.
What trips people up
A requirement is matched against the placeholder only, and the pattern is anchored. Writing .+ as a requirement lets that placeholder swallow slashes and the rest of the URL with them.
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;
final class ArticleController extends AbstractController
{
// priority defaults to 0; 1 means this route is tried first,
// so it cannot be shadowed by the /{id} route below.
#[Route('/articles/new', name: 'app_article_new', priority: 1, methods: ['GET'])]
public function new(): Response
{
return new Response('<h1>New article</h1>');
}
// Shorthand requirement: {id<\d+>} matches digits only.
// Longhand for the same route:
// #[Route('/articles/{id}', name: 'app_article_show',
// requirements: ['id' => '\d+'])]
#[Route('/articles/{id<\d+>}', name: 'app_article_show', methods: ['GET'])]
public function show(int $id): Response
{
return new Response(sprintf('Article %d', $id));
}
}
When to use it
- Stop /articles/{id} from matching a non-numeric id and reaching a method typed for int.
- Keep an /articles/new form reachable when /articles/{id} is declared in the same class.
- Restrict a path segment to a fixed set of values such as daily|weekly|monthly.
More examples
An enumerated requirement
A requirement is an ordinary regular expression, so alternation works. `GET /reports/weekly` returns 200; `GET /reports/hourly` returns 404 without the controller ever running. Use this for a small closed set of values that belong in the path rather than the query string.
<?php
// src/Controller/ReportController.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 ReportController extends AbstractController
{
// Only three values reach the method; anything else is a 404,
// which means no validation code is needed inside it.
#[Route(
'/reports/{period<daily|weekly|monthly>}',
name: 'app_report',
methods: ['GET'],
)]
public function show(string $period): Response
{
return new Response('Report: ' . $period);
}
}
A requirement that deliberately allows slashes
The one legitimate reason to allow slashes in a placeholder: a hierarchical document path. The negative priority is the point - a greedy route is tried after everything else, so it cannot steal `/docs/search`. `GET /docs/guide/routing` returns 200 with `Doc page: guide/routing`.
<?php
// src/Controller/DocsController.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 DocsController extends AbstractController
{
// .+ lets the placeholder span slashes, so this matches
// /docs/install and /docs/guide/routing/basics alike.
// Because it is greedy, keep it last and give more specific
// /docs/... routes a higher priority.
#[Route('/docs/{path<.+>}', name: 'app_docs', priority: -10, methods: ['GET'])]
public function show(string $path): Response
{
return new Response('Doc page: ' . $path);
}
}
Requirements in YAML
In a YAML file, routes are also matched in the order they appear, so moving `app_article_new` above `app_article_show` would fix the collision by position alone. Pattern-constraining the id is still the safer fix, because it survives someone reordering the file.
# config/routes.yaml
app_article_show:
path: /articles/{id}
controller: App\Controller\ArticleController::show
methods: GET
requirements:
id: '\d+'
app_article_new:
path: /articles/new
controller: App\Controller\ArticleController::new
methods: GET
Discussion