Writing Controllers
Writing Controllers
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
A controller is, at its core, a SIMPLE PHP method: it takes a Request (implicitly or explicitly) and returns a Response. AbstractController provides useful helper methods along the way, which we'll get to know now.
AbstractController: useful helper methods
Symfony controllers typically extend AbstractController – NOT mandatory (any callable works as a controller), but it saves recurring boilerplate:
$this->render(...)– render a Twig template (chapter 13).$this->redirectToRoute(...)– redirect to another route (chapter 11).$this->json(...)– produce a JSON response (chapter 11).$this->getUser()– get the logged-in user (block 5).$this->addFlash(...)– set a flash message (chapter 12).
For now: working with hardcoded sample data
Doctrine only follows in block 4 – until then, we deliberately work with a simple, hardcoded array, so we can focus on routing/controllers/Twig without also needing a database:
<?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 ProjectController extends AbstractController
{
private const array SAMPLE_PROJECTS = [
['id' => 1, 'name' => 'Website Relaunch'],
['id' => 2, 'name' => 'Mobile App'],
['id' => 3, 'name' => 'Internal Tools'],
];
#[Route('/projects', name: 'project_index', methods: ['GET'])]
public function index(): Response
{
$output = 'Projects:';
foreach (self::SAMPLE_PROJECTS as $project) {
$output .= sprintf("\n- %s", $project['name']);
}
return new Response($output, Response::HTTP_OK, [
'Content-Type' => 'text/plain',
]);
}
}Response::HTTP_OK is a named constant instead of the "magic number" 200 – Symfony's Response class defines constants for ALL common HTTP status codes, which considerably improves readability.
Dependencies via constructor property promotion
If a controller needs a service (block 6 covers this systematically), it gets automatically injected into the constructor via autowiring – we don't need to register ANYTHING manually:
// ... use statements as above ...
use Psr\Log\LoggerInterface;
class ProjectController extends AbstractController
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
#[Route('/projects', name: 'project_index', methods: ['GET'])]
public function index(): Response
{
$this->logger->info('Project list accessed');
return new Response('Loading projects...');
}
}LoggerInterface is already available as a service from the default installation (Monolog). Chapters 32-34 explain the autowiring principle behind this in detail – for now, it's enough to know: ANY type declaration in the constructor that Symfony knows a matching service definition for gets resolved AUTOMATICALLY.
Multiple actions per controller
A controller may (and typically should) bundle SEVERAL related routes – for our project: one class per domain concept, several methods ("actions") per class:
class ProjectController extends AbstractController
{
#[Route('/projects', name: 'project_index', methods: ['GET'])]
public function index(): Response { /* ... */ }
#[Route('/projects/new', name: 'project_new', methods: ['GET', 'POST'])]
public function new(): Response { /* ... */ }
#[Route('/projects/{id}', name: 'project_show', methods: ['GET'])]
public function show(int $id): Response { /* ... */ }
}Tipp: #[Route('/projects', name: 'project_')] can additionally be set at the CLASS level, to give all actions a shared path/name prefix – handy once a controller grows. We'll introduce this once ProjectController has several routes.