Drupalwoo ☉

Building a Drupal 10 Module with Services and Dependency Injection

Drupal 10 inherits the service container architecture that powered Drupal 8 and 9, giving site builders a robust mechanism for organising reusable business logic. For developers working across Australian digital agencies, from Sydney-based e-commerce platforms to Brisbane government portals, mastering dependency injection means writing code that is testable, maintainable, and aligned with modern PHP practices.

Dependency injection is a pattern where a component receives its dependencies from an external source rather than creating them internally. In Drupal, that external source is the Symfony-based service container, which manages the lifecycle of every service defined in the codebase. By relying on this container, modules become loosely coupled and significantly easier to swap out or mock during automated testing.

This walkthrough covers the creation of a custom Drupal 10 module that registers its own services, injects dependencies through constructor arguments, and configures those services using YAML files. Whether you maintain a university site in Melbourne or a tourism portal in Perth, the workflow remains consistent across the continent.

Understanding the Drupal Service Container

Drupal's service container is built on top of Symfony's dependency injection component. Every time a request arrives at the kernel, the container compiles a list of services from module configuration files and core definitions. These services are instantiated lazily or eagerly, depending on how they are declared.

The container resolves dependencies by inspecting type hints in service constructors. When you write public function __construct(ClientInterface $http_client), the container searches for any service tagged with the same interface and injects it automatically. This autowiring feature reduces boilerplate and prevents the common mistake of manually instantiating classes inside services.

Australian developers building federal or state projects frequently need to follow the Digital Transformation Agency's Service Standard, which emphasises security by design. Using the service container makes it easier to swap implementations during security audits, such as replacing a logging service with one that writes to a secure Sydney-based SIEM. The maintainers at Drupalwoo regularly share related tips on their Drupalwoo tweets feed.

Setting Up the Module Structure

A well-structured module contains consistent locations for source files, metadata, and configuration. For a module called drupalwoo_custom, the layout typically includes a drupalwoo_custom.info.yml file describing the module, a drupalwoo_custom.module file for procedural hooks, and a src directory holding PHP classes organised by namespace.

The src folder mirrors PHP namespace conventions, so a class called Drupal\drupalwoo_custom\Service\GreetingService lives at src/Service/GreetingService.php. Keeping services in a dedicated subdirectory makes them easy to locate during code reviews, which is particularly valuable for distributed teams collaborating across time zones from Adelaide to Darwin.

Australian Privacy Principles under the Privacy Act 1988 require organisations to handle personal information transparently. When a module processes user data, placing that logic inside injectable services rather than scattered procedural code makes it simpler to audit and document, satisfying compliance reviewers with clear separation of concerns.

Creating Services with Constructor Injection

Create a new PHP class inside the src/Service directory. The class accepts its dependencies through the constructor and stores them in private properties. Drupal's autowiring handles the rest, provided the dependencies are also registered services or implement interfaces available in the container.

namespace Drupal\drupalwoo_custom\Service;

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;

class ApiLoggerService {

  private $client;
  private $logger;

  public function __construct(ClientInterface $client, LoggerChannelFactoryInterface $logger_factory) {
    $this->client = $client;
    $this->logger = $logger_factory->get('drupalwoo_custom');
  }

  public function logRequest(string $url): array {
    $response = $this->client->request('GET', $url);
    $this->logger->info('Requested URL: @url', ['@url' => $url]);
    return [
      'status' => $response->getStatusCode(),
      'body' => (string) $response->getBody(),
    ];
  }

}

Notice how the class does not instantiate GuzzleHttp\Client directly. Instead, it receives an implementation of ClientInterface from the container. This makes the class trivial to unit test by passing a mock client during PHPUnit runs.

To access the service from controllers, forms, or plugins, use constructor injection in those classes rather than calling \Drupal::service() statically. Controllers extending ControllerBase can use the built-in service() helper, but constructor injection remains the preferred approach for new code because it avoids hidden coupling to the global container.

Configuring Services via YAML

Although Drupal can autowire many services automatically, modules often need to declare aliases, tags, or factory parameters. This is done through a drupalwoo_custom.services.yml file placed in the module root.

services:
  drupalwoo_custom.api_logger:
    class: Drupal\drupalwoo_custom\Service\ApiLoggerService
    arguments:
      - '@http_client'
      - '@logger.factory'

Each argument uses the @ symbol to reference another service by its machine name. The http_client service comes from the Guzzle module that ships with Drupal core, while logger.factory is provided by the logging system. Modules can also override core services using the same syntax, though doing so requires care to avoid breaking upstream functionality.

For developers maintaining sites that fall under the Australian Cyber Security Centre's Essential Eight mitigation strategies, replacing insecure HTTP clients with ones that enforce TLS 1.3 and strict certificate validation becomes a configuration concern rather than a code rewrite when services are properly injected.

Comparing Service Registration Approaches

Different registration methods suit different situations. The table below summarises the trade-offs between autowiring, explicit YAML declaration, and static factory instantiation.

Method Discoverability Testability Maintenance Overhead Best Suited For
Autowiring only High for simple services Excellent Low overhead Full blade servers with predictable dependencies
Explicit YAML High with full visibility Excellent Medium Services needing tags, aliases, or decorators
Static \Drupal::service() Low coupling to global state Poor without refactoring Legacy patterns Quick prototypes inside procedural hook code

For long-running production systems, explicit YAML combined with autowiring offers the best balance. For experimental modules still in the prototyping phase, autowiring alone accelerates development. Static service retrieval remains acceptable only inside .module hook implementations where constructor injection is impossible.

Practical Recommendations for Service Design

Following established patterns keeps modules maintainable and ready for handoff between team members working in Perth, Canberra, or remotely from regional Queensland.

Module authors who follow these guidelines produce code that integrates smoothly with Drupal core, plays well with contributed modules, and survives the long lifecycle of government and enterprise projects that often span five years or more across multiple Australian jurisdictions.