You've read the anatomy of a controller from the outside — the route that points to it, the string that names it. Now it's time to actually write one. A controller class is deliberately simple: extend a base class, write one or more public methods, and return a render array from each. This lesson builds a real one from scratch, and shows a huge shortcut most developers don't discover for a while: Drupal can generate this entire skeleton for you.
What you'll learn in this lesson
- The minimal shape of a controller class extending
ControllerBase - What
ControllerBasegives you for free - How to scaffold a working route + controller pair with one Drush command
A real, minimal controller
<?php
namespace Drupal\page_example\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller routines for page example routes.
*/
class PageExampleController extends ControllerBase {
/**
* Constructs a simple page.
*/
public function simple() {
return [
'#markup' => '<p>' . $this->t('Simple page: The quick brown fox jumps over the lazy dog.') . '</p>',
];
}
}
That's genuinely the whole shape: a class, a namespace matching its file path, extending ControllerBase, with one public method returning a render array. Nothing else is required to make this a working controller.
What ControllerBase gives you for free
Extending ControllerBase isn't mandatory, but it saves you from re-plumbing common services yourself. Out of the box, it gives you $this->t() for translatable strings, $this->currentUser() for the logged-in user, $this->entityTypeManager() for loading entities, $this->config() for reading configuration, and several more — all lazily instantiated, so you only pay for the services you actually call.
The fast way: let Drush generate this for you
Writing a route and a matching controller class by hand is a fine way to learn the mechanics, but in daily work most developers scaffold both at once with Drush's code-generation commands. Running the generator produces a working .routing.yml entry and a matching controller class in one step, wired together correctly from the start.
Quick check: why extend
ControllerBaseinstead of just writing a plain PHP class with asimple()method? A plain class would technically still work as long as its method returns a render array — but you'd lose every convenience method ($this->t(),$this->currentUser(), and the rest) and have to inject each service yourself by hand.
Key takeaways
- A controller class needs only a namespace matching its file path and one public method returning a render array — extending
ControllerBaseis a convenience, not a strict requirement. ControllerBaseprovides lazily-loaded access to common services like translation, the current user, entity storage, and configuration.- Drush's code generators can scaffold a matching route and controller class together in one command — a real time-saver once you understand the underlying mechanics.
- Always review generated code before shipping it — the generator handles structure, not your actual logic.
Coming up next
You've seen #markup used for the simplest possible output. The next lesson, Returning Render Arrays, goes deeper into the other keys a render array can hold and when to reach for each one.