Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.

Routing is the layer that decides which controller runs for a given URL. Symfony used to keep routes in YAML files, but modern Symfony puts them right on the controller method as PHP attributes, so the URL lives next to the code that handles it. Once you know the #[Route] attribute and its options, routing is one of the simplest parts of the framework.

A basic route

You attach #[Route] to a controller method, giving it a path and a name:

use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Response;

class ProductController
{
    #[Route('/products', name: 'product_list')]
    public function list(): Response
    {
        // ...
    }
}

The path matches the URL, and the name is an internal identifier you use to generate links to this route, so the URL string lives in one place.

Path parameters

Curly braces capture parts of the URL. Type-hint the matching method argument and Symfony passes the value in, converted to the right type:

#[Route('/products/{id}', name: 'product_show')]
public function show(int $id): Response
{
    // /products/42 gives you $id = 42
}

Requirements and methods

You can constrain a parameter with a regular expression, so {id} only matches numbers and a non-numeric URL falls through to another route instead of erroring:

#[Route('/products/{id}', name: 'product_show', requirements: ['id' => '\d+'])]

And you restrict which HTTP methods a route answers, which is how you point the same path at different actions for GET and POST:

#[Route('/products', name: 'product_create', methods: ['POST'])]

A prefix for the whole controller

Put a #[Route] on the controller class and every method's path is prefixed with it, which keeps related routes tidy:

#[Route('/admin')]
class AdminProductController
{
    #[Route('/products', name: 'admin_product_list')] // becomes /admin/products
    public function list(): Response {}
}

Generating URLs

You never hardcode a URL elsewhere in the app. You generate it from the route name, so changing the path in one place updates every link. In a controller:

$url = $this->generateUrl('product_show', ['id' => 42]);

And in a Twig template:

<a href="{{ path('product_show', { id: product.id }) }}">View</a>

Because links are built from names, you can rename a path freely without hunting down every reference.

Seeing your routes

php bin/console debug:router lists every route, its path, methods, and name. It is the quickest way to confirm a route is registered and to find the name you need for generateUrl or path.

Wrapping up

Symfony routing lives in #[Route] attributes on your controller methods: a path with {placeholders}, an internal name, optional requirements to constrain parameters, and methods to limit HTTP verbs. A class-level route prefixes a whole controller, and you always build links from route names with generateUrl or Twig's path, never hardcoded strings. Keep debug:router handy and routing stays a small, predictable part of the app.