Creating a Controller Class in Drupal: Extending ControllerBasefor Drupal 8 , 9 , 10 , and 11

Last updated :  

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 ControllerBase gives 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.

Drupal Custom Module Routing and controller using drush command Drupal Custom Module Routing and controller using drush command Result
Worth knowing: generated code is a starting point, not a finished product. Always open the generated files afterward and read through them — the generator can't know your actual business logic, only the correct scaffolding to hold it.

Quick check: why extend ControllerBase instead of just writing a plain PHP class with a simple() 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 ControllerBase is a convenience, not a strict requirement.
  • ControllerBase provides 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.