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
StringTranslationTraitandMessengerTraitinstead 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.
Quick check: why does this class use
MessengerTraitinstead of directly injecting@messengeras 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
StringTranslationTraitandMessengerTraitfor 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.