Route Parameters

Placeholders in a route path capture part of the URL and arrive as controller method arguments of the same name.

Syntax#[Route('/articles/{page?1}', name: 'app_article_page')]

Most URLs contain a value: an article id, a slug, a page number. A placeholder is the part of a route path written in braces, such as {slug}. It matches one segment of the URL and captures what it matched.

How the value reaches your code

The captured value is handed to the controller method as an argument with the same name. The match is by name, not by position: a path of /authors/{author}/articles/{slug} fills $author and $slug whatever order you declare them in. A name that does not correspond to any argument is still captured, it simply is not passed.

Defaults and optional segments

A placeholder can have a default value, which makes the segment optional. The shorthand puts the default inside the braces after a question mark: {page?1}. The longhand passes a defaults array to the attribute: defaults: ['page' => 1]. They are the same thing, and both make /articles/page match with $page equal to 1. Declare the same default on the method argument too, so the method is still callable directly.

What trips people up

A placeholder does not match a slash. /files/{path} will never match /files/a/b.txt, because {path} stops at the first slash. Only trailing placeholders can be optional: in /{year}/{month}, giving year a default and month none leaves a path that cannot be built.

Example

Example · php
<?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
{
    // {slug} captures one path segment and arrives as $slug.
    #[Route('/articles/{slug}', name: 'app_article_show', methods: ['GET'])]
    public function show(string $slug): Response
    {
        return new Response(sprintf('Article: %s', $slug));
    }

    // Shorthand: {page?1} means "optional, default 1".
    // Longhand for the same route:
    // #[Route('/articles/page/{page}', name: 'app_article_page',
    //     defaults: ['page' => 1])]
    #[Route('/articles/page/{page?1}', name: 'app_article_page', methods: ['GET'])]
    public function page(int $page = 1): Response
    {
        return new Response(sprintf('Listing page %d', $page));
    }
}

When to use it

  • Serve /articles/{slug} from a single controller method instead of one route per article.
  • Paginate a listing where /articles/page with no number means page 1.
  • Nest a resource under its owner, such as /authors/{author}/articles/{slug}.

More examples

Two placeholders, declared in any order

Shows that placeholder-to-argument matching is by name. `GET /authors/ada/articles/on-engines` returns 200 with the body `on-engines by ada`. Useful whenever a resource only makes sense underneath another one in the URL.

Example · php
<?php
// src/Controller/AuthorArticleController.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 AuthorArticleController extends AbstractController
{
    #[Route(
        '/authors/{author}/articles/{slug}',
        name: 'app_author_article',
        methods: ['GET'],
    )]
    public function show(string $slug, string $author): Response
    {
        // $slug is declared first, $author second, and the path
        // lists them the other way round. Matching is by name.
        return new Response(sprintf('%s by %s', $slug, $author));
    }
}

Optional trailing segments with explicit defaults

Optional segments are driven entirely by defaults, and they only work from the right-hand end inwards. `GET /archive` returns 200 with `Archive: all`; `GET /archive/2026/03` returns `Archive: 2026-03`. Reach for this when one controller should serve a whole/year/month drill-down rather than three near-identical routes.

Example · php
<?php
// src/Controller/ArchiveController.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 ArchiveController extends AbstractController
{
    // Both placeholders default to null, so three URLs match:
    // /archive, /archive/2026 and /archive/2026/03
    #[Route(
        '/archive/{year}/{month}',
        name: 'app_archive',
        defaults: ['year' => null, 'month' => null],
        methods: ['GET'],
    )]
    public function index(?string $year = null, ?string $month = null): Response
    {
        $label = $month !== null ? $year . '-' . $month : ($year ?? 'all');

        return new Response('Archive: ' . $label);
    }
}

The same parameterised route in YAML

The YAML `defaults` block is the longhand of the inline `{page?1}` shorthand. Seeing both side by side matters because generated and third-party routing files use the long form, while hand-written attributes usually use the short one.

Example · yaml
# config/routes.yaml
app_article_page:
    path: /articles/page/{page}
    controller: App\Controller\ArticleController::page
    methods: GET
    defaults:
        page: 1

Discussion

  • Be the first to comment on this lesson.