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

At its heart Symfony does one thing: it takes an HTTP Request and produces an HTTP Response. Everything else, routing, controllers, templates, is in service of that. Understanding that flow, and where your controller sits in it, makes the framework click.

The lifecycle in brief

When a request arrives, the HttpKernel drives it through a few stages. It works out which controller matches the URL using routing, resolves the controller's arguments, calls your controller, and expects a Response back, which it then sends. Along the way it dispatches events, kernel.request, kernel.controller, kernel.response, that let you hook into the flow, but for everyday work you mostly care about one stage: your controller turning the request into a response.

A controller is Request in, Response out

A controller is any callable that returns a Response. In practice it is a method on a class extending AbstractController, which gives you a set of convenient helpers:

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    #[Route('/products/{id}', name: 'product_show')]
    public function show(int $id, ProductRepository $products): Response
    {
        $product = $products->find($id);

        if (!$product) {
            throw $this->createNotFoundException('No product found.');
        }

        return $this->render('product/show.html.twig', ['product' => $product]);
    }
}

Notice the ProductRepository argument. You inject services straight into the action by type-hinting them, the same autowiring as constructor injection, but per method. Anything the action needs, you ask for in its signature.

The helpers you will use

AbstractController gives you shortcuts for the common responses:

return $this->render('template.html.twig', ['key' => $value]); // HTML
return $this->json(['status' => 'ok']);                         // JSON
return $this->redirectToRoute('product_list');                 // redirect
throw $this->createNotFoundException();                         // 404
$this->addFlash('success', 'Saved.');                          // flash message
$user = $this->getUser();                                       // current user

Each ultimately returns a Response object. That is the one rule of a controller: it must return a Response, whether that is a JsonResponse, a RedirectResponse, or the HTML that render produces.

Reading the request

When you need the incoming data, type-hint the Request:

public function search(Request $request): Response
{
    $term = $request->query->get('q');        // ?q=...
    $name = $request->request->get('name');   // POST body field
    $body = $request->getContent();           // raw body, e.g. JSON
    // ...
}

query holds the query string, request holds form-posted fields, and getContent gives you the raw body for APIs.

Fetching entities automatically

A nice shortcut: instead of taking an int $id and looking up the entity yourself, type-hint the entity directly and Symfony fetches it for you from the route parameter:

#[Route('/products/{id}', name: 'product_show')]
public function show(Product $product): Response
{
    return $this->render('product/show.html.twig', ['product' => $product]);
}

If no product matches the id, Symfony throws a 404 automatically. It is less code for the most common case.

Wrapping up

Symfony is a Request-to-Response machine, and the kernel runs that flow: route the request, resolve the controller's arguments, call it, send the Response it returns. Your controller extends AbstractController for helpers like render, json, and redirectToRoute, injects services by type-hinting them in the action, reads input from the Request, and can have entities fetched for it automatically. Keep "every action returns a Response" in mind and controllers stay simple.