Building a PHP class for the custom servicefor Drupal 11 , 10 , 9 , and 8

Last updated :  

You've registered a service in YAML and seen how its dependencies get matched by position. Now let's build the actual PHP class those two lessons have been pointing at — using the real, complete DbtngExampleRepository class from Drupal's Examples module as the template.

What you'll learn in this lesson

  • The minimal shape every service class needs: properties, a constructor, and public methods
  • Why service classes lean on traits like StringTranslationTrait and MessengerTrait instead of reimplementing common behavior
  • How to group related logic into one cohesive service rather than scattering it across helper functions

A real service class, in full

namespace Drupal\dbtng_example;

use Drupal\Core\Database\Connection;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;

class DbtngExampleRepository {

  use MessengerTrait;
  use StringTranslationTrait;

  protected $connection;

  public function __construct(Connection $connection, TranslationInterface $translation, MessengerInterface $messenger) {
    $this->connection = $connection;
    $this->setStringTranslation($translation);
    $this->setMessenger($messenger);
  }

  public function insert(array $entry) {
    // ... uses $this->connection to write a row ...
  }

}

Properties hold what the constructor received

Every injected dependency gets stored as a protected property in the constructor, exactly once. Every other method in the class then reads $this->connection rather than re-requesting the service — the whole point of injection is that the dependency arrives once, up front.

Traits give you common behavior for free

StringTranslationTrait and MessengerTrait are how a plain PHP class (not a Controller, not a Form — those get this automatically) gains access to $this->t() and $this->messenger(). Rather than injecting the string_translation and messenger services and writing your own getter methods, the trait provides a standard setStringTranslation()/setMessenger() pair that your constructor calls once.

Keep one service focused on one responsibility. It's tempting to let a service grow into a dumping ground for every helper method your module needs. A repository service should only do CRUD against its table; a formatter service should only format. When a class starts injecting five or six unrelated services, that's usually a sign it should be split into two smaller ones.

Quick check: why does this class use MessengerTrait instead of directly injecting @messenger as a constructor argument like it does for @database? (Answer: both approaches work technically, but the trait pattern is Drupal's established convention specifically for the messenger and string-translation services, because it standardizes the setter method name and keeps the constructor argument list focused on the service's true unique dependencies.)

Key takeaways

  • Store every injected dependency as a property inside the constructor — never re-request a service mid-method.
  • Reach for StringTranslationTrait and MessengerTrait for translation and messaging rather than hand-rolling getters.
  • Keep each service narrowly scoped to one responsibility; a growing constructor argument list is a signal to split the class.

Coming up next

A service sitting in isolation is only useful once something else actually calls it. Next, we'll inject this exact kind of service into a Controller's constructor and see the full request-to-response path in action.