Anti-Patterns & Common Mistakes
When to Use
When you want to avoid the most common mistakes developers make with Drupal services and dependency injection.
Anti-Patterns
1. Container Injection (Service Locator)
Anti-Pattern:
class MyService {
public function __construct(
protected ContainerInterface $container,
) {}
public function doSomething() {
$db = $this->container->get('database'); // BAD
$logger = $this->container->get('logger.factory')->get('my_module'); // BAD
}
}
Correct:
class MyService {
public function __construct(
protected Connection $database,
protected LoggerInterface $logger,
) {}
public function doSomething() {
$this->database->select(...); // GOOD
$this->logger->info('message'); // GOOD
}
}
Why: Hides dependencies, makes testing harder, defeats the purpose of DI.
2. Logic in Constructors
Anti-Pattern:
public function __construct(
protected Connection $database,
) {
$this->results = $this->database->query('SELECT * FROM {node}')->fetchAll(); // BAD
$this->config = \Drupal::config('system.site'); // BAD
}
Correct:
public function __construct(
protected Connection $database,
protected ConfigFactoryInterface $configFactory,
) {
// Only assign dependencies — no logic
}
public function getResults() {
return $this->database->query('SELECT * FROM {node}')->fetchAll();
}
Why: Constructors should only assign dependencies. Logic in constructors can cause issues during container compilation, testing, and service instantiation.
3. Using \Drupal in Classes
Anti-Pattern:
class MyController extends ControllerBase {
public function content() {
$db = \Drupal::database(); // BAD
$config = \Drupal::config('system.site'); // BAD
return ['#markup' => 'Hello'];
}
}
Correct:
class MyController extends ControllerBase {
use AutowireTrait;
public function __construct(
protected Connection $database,
protected ConfigFactoryInterface $configFactory,
) {}
public function content() {
// Use $this->database, $this->configFactory
return ['#markup' => 'Hello'];
}
}
Why: \Drupal is for procedural code only. Classes should use dependency injection.
4. Eager Service Collection
Anti-Pattern:
services:
my_module.manager:
tags:
- { name: service_collector, tag: my_handler } # Instantiates ALL handlers immediately
Correct:
services:
my_module.manager:
arguments: [!tagged_iterator my_handler] # Lazy — instantiates on iteration
Why: service_collector eagerly creates all tagged services. Use !tagged_iterator for lazy loading.
5. Not Type-Hinting Interfaces
Anti-Pattern:
public function __construct(
protected EntityTypeManager $entityTypeManager, // Concrete class
protected LoggerChannel $logger, // Concrete class
) {}
Correct:
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager, // Interface
protected LoggerInterface $logger, // Interface
) {}
Why: Type-hinting interfaces allows service swapping, easier mocking, follows dependency inversion principle.
6. Circular Dependencies
Anti-Pattern:
services:
my_module.service_a:
arguments: ['@my_module.service_b']
my_module.service_b:
arguments: ['@my_module.service_a'] # Circular dependency
Solution: - Extract shared logic to a third service - Use event system instead of direct dependency - One service can use the other via factory or lazy proxy
Why: Container can't resolve circular dependencies; will throw exception.
7. Public Properties for Services
Anti-Pattern:
class MyService {
public Connection $database; // BAD
public function __construct(Connection $database) {
$this->database = $database;
}
}
Correct:
class MyService {
public function __construct(
protected Connection $database, // GOOD
) {}
}
Why: Services are internal dependencies; should be protected. Exposing them publicly invites misuse.
8. Not Using AutowireTrait
Anti-Pattern:
class MyController extends ControllerBase implements ContainerInjectionInterface {
public function __construct(
protected Connection $database,
protected LoggerInterface $logger,
) {}
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('logger.channel.my_module'),
);
}
}
Correct:
class MyController extends ControllerBase {
use AutowireTrait; // Generates create() automatically
public function __construct(
protected Connection $database,
protected LoggerInterface $logger,
) {}
}
Why: AutowireTrait eliminates boilerplate. Use it unless you need custom create() logic.
Reference: Dependency injection anti-patterns in Drupal
Common Mistakes
- Forgetting cache rebuild — Service changes require
drush cr - Mixing procedural and OOP — Hooks use \Drupal, classes use DI; don't mix
- Over-complicating — Not everything needs DI; simple value objects are fine as-is
- Ignoring deprecation warnings — Service deprecations will break in next major version
- Testing in prod — Validate service definitions work via automated tests