Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:

src/Controller/ProjectController.php
<?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:

src/Controller/ProjectController.php
// ... 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.