Injecting services into controllers via constructorfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Controllers are where your service actually gets put to work — a real HTTP request comes in, and the controller leans on the service to do the heavy lifting instead of doing it inline. Here's the real, complete pattern from Drupal's Examples module.

What you'll learn in this lesson

  • Why ControllerBase needs a static create() method to receive services at all
  • How to store an injected service and use it inside your controller's action method
  • The difference between injecting via the constructor and reaching for \Drupal::service() mid-method

A real controller with an injected service

namespace Drupal\dbtng_example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\dbtng_example\DbtngExampleRepository;
use Symfony\Component\DependencyInjection\ContainerInterface;

class DbtngExampleController extends ControllerBase {

  protected $repository;

  public static function create(ContainerInterface $container) {
    $controller = new static($container->get('dbtng_example.repository'));
    $controller->setStringTranslation($container->get('string_translation'));
    return $controller;
  }

  public function __construct(DbtngExampleRepository $repository) {
    $this->repository = $repository;
  }

}

Why create() exists at all

Drupal's routing system doesn't build your controller with new DbtngExampleController() — it calls the static create() method instead, handing it the entire service container. This one indirection is what lets your controller ask for exactly the services it needs, by name, rather than the router having to know in advance what every controller in every module requires.

The constructor stays simple

Notice the constructor itself knows nothing about the container — it just receives a plain DbtngExampleRepository object and stores it. All the container-specific code lives in create(). This separation means you (or a test) can construct the controller directly with a mock repository, without ever touching the real service container.

Constructor injection vs. \Drupal::service(). You'll sometimes see code reach for \Drupal::service('dbtng_example.repository') directly inside a method body instead of injecting it. That works, but it hides the dependency (you can't tell what a class needs just by reading its constructor) and makes the class much harder to unit test, since you can't substitute a fake service. Constructor injection, as shown above, is the pattern Drupal core itself uses everywhere and the one you should default to.

Quick check: what would happen if you deleted the create() method entirely but kept the constructor requiring a DbtngExampleRepository argument? (Answer: Drupal's default controller-resolution falls back to calling the constructor with zero arguments, which would immediately throw a fatal "too few arguments" error the first time the route is visited — `create()` is what makes constructor injection actually work for routed controllers.)

Key takeaways

  • Routed controllers receive services via a static create(ContainerInterface $container) method, not via Drupal calling their constructor directly.
  • Keep the constructor itself container-agnostic — it should just accept and store plain typed objects.
  • Prefer constructor injection over calling \Drupal::service() inside a method — it documents dependencies openly and keeps the class testable.

Coming up next

Controllers aren't the only place services get injected — plugins, event subscribers, and plain helper classes all need the same access. Next, we'll look at how the exact same create() pattern shows up outside of controllers and forms.