Your First Page

A controller class, a route attribute and a Response object: the smallest complete Symfony page, line by line.

Syntax#[Route('/path', name: 'app_route_name', methods: ['GET'])]

A controller is a method you write that answers a request. It takes whatever it needs as arguments and returns a Response object: a status code, headers and a body. Nothing else is required to serve a page.

How the URL finds the method

The link between a URL and a method is a route. In Symfony 8 you declare it with a PHP attribute directly above the method:

#[Route('/articles', name: 'app_article_index', methods: ['GET'])]

The first argument is the path. name is an internal label used to generate URLs later, so other code never has to repeat the path. methods restricts which HTTP verbs match. The attribute is shorthand for an entry in config/routes.yaml; both produce the same route, and php bin/console debug:router lists them side by side.

Why it is found at all

Classes in src/Controller/ are scanned for route attributes by the configuration a new project ships with. Put the same class elsewhere and the route will not exist until you tell Symfony to look there.

Returning the response

Extending AbstractController is optional. It adds helpers: render() for a Twig template, json() for a JSON body, redirectToRoute() for a redirect. Those three return a Response. createNotFoundException() is the exception: it builds a NotFoundHttpException, which you throw rather than return, and the kernel turns it into a 404 response.

What trips people up

The method must return the response. Printing with echo sends bytes immediately, before Symfony has had the chance to set a status code or any header.

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;

class ArticleController extends AbstractController
{
    // Path, internal name, allowed verbs. A POST to /articles will not
    // match this route and returns 405.
    #[Route('/articles', name: 'app_article_index', methods: ['GET'])]
    public function index(): Response
    {
        $body = '<html><body><h1>Articles</h1></body></html>';

        // Longhand: status and headers spelled out.
        return new Response(
            $body,
            Response::HTTP_OK,
            ['Content-Type' => 'text/html; charset=utf-8'],
        );

        // Shorthand for exactly the same response, since 200 and an
        // HTML content type are the defaults:
        // return new Response($body);
    }
}

When to use it

  • Serve a first page from a new project to confirm the installation works end to end.
  • Add a /status endpoint that returns JSON for an uptime check.
  • Work out why a new URL returns 404 by comparing the attribute against debug:router.

More examples

The same route written as configuration

The attribute and this YAML entry create the identical route; the attribute keeps the declaration next to the code, the file keeps it greppable in one place. The file form is what you need when the controller lives in a package you cannot edit.

Example · yaml
# config/routes.yaml
# Longhand equivalent of the #[Route] attribute above. Define a route
# one way or the other, not both, or the names collide.

app_article_index:
    path: /articles
    controller: App\Controller\ArticleController::index
    methods: [GET]

A JSON response, shorthand and longhand

Both routes return 200 with the body {"status":"ok"} and a JSON content type. Recognising that $this->json() is JsonResponse matters the moment you need something the helper does not expose, such as a custom status code with specific headers.

Example · php
<?php
// src/Controller/StatusController.php
declare(strict_types=1);

namespace App\Controller;

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

class StatusController extends AbstractController
{
    #[Route('/status', name: 'app_status', methods: ['GET'])]
    public function status(): Response
    {
        // Shorthand from AbstractController.
        return $this->json(['status' => 'ok']);
    }

    #[Route('/status/long', name: 'app_status_long', methods: ['GET'])]
    public function statusLonghand(): Response
    {
        // What the line above produces: a Response subclass that encodes
        // the array and sets Content-Type: application/json.
        return new JsonResponse(['status' => 'ok']);
    }
}

Listing the routes the application actually has

The first command to run when a URL returns 404. If the route is absent from this list, the problem is discovery — wrong directory, or a docblock instead of an attribute — not the controller body.

Example · bash
php bin/console debug:router

#  -------------------- -------- -------- ------ -----------
#   Name                 Method   Scheme   Host   Path
#  -------------------- -------- -------- ------ -----------
#   app_article_index    GET      ANY      ANY    /articles
#   app_status           GET      ANY      ANY    /status
#   app_status_long      GET      ANY      ANY    /status/long
#  -------------------- -------- -------- ------ -----------

Discussion

  • Be the first to comment on this lesson.