Dependency injection pattern when using services-based containers: a trait per dependency

When you're using a framework that provides you with good dependency injection, like Symfony's services, then it's helpful to have patterns that keep your DI as clean and testable as possible. But if you aren't injecting your entire container into a service, then how can you keep track of all the other services you need to inject explicitly instead?

For each commonly injected service, I usually define a dedicated PHP trait:

<?php
 
namespace MyApp\Di;
 
use MyApp\Service\SpoonBender;
 
/**
 * Provides ability to bend spoons.
 *
 * @package MyApp\Di;
 */
trait BendsSpoonTrait
{
    /**
     * @var SpoonBender
     */
    protected $spoonBender;
 
    /**
     * @param SpoonBender $spoonBender
     *   Service that bends spoons.
     */
    public setSpoonBender(SpoonBender $spoonBender)
    {
        $this->spoonBender = $spoonBender;
    }
}

You can then drop this into the top of any class as follows:

use MyApp\Di;
 
class Magician
{
    use Di\BendsSpoonTrait;
}

and add a line to the services.yml:

app.performer.magician:
    class: MyApp\Performer\Magician
    calls:
        - [setSpoonBender, ['@app.service.spoon_bender']]

Every time you want to inject an already managed service into a new other-service, you now only need one or two lines of PHP code, and one line in a YAML file. There doesn't need to be any further mention of the SpoonBender class cluttering up your code: an extra boon for when you inevitably refactor.

Like a lot of conventions, this one really benefits from the implicit naming conventions that inform the code snippets above. These include (but are not limited to):

  • Place all DI traits in the Di subfolder of your codebase.
  • Name traits after verb phrases in the present tense e.g. "BendsSpoon" and end with "Trait". This shows they're giving the class a power to do a thing, not just providing it with some catch-all service. Avoid generic verbs like "Provides" or "Enables".
  • Only include a setter and a protected variable in each trait. Anything else is not related to DI.
  • Name objects using BumpyCaps, and methods and variables using camelBack.
  • Ensure consistency across service names, setter method names, variable names, trait names etc. Avoid switching from singular to plural arbitrarily (tip: use singular throughout.)
  • Use words from the ubiquitous vocabulary of the domain of knowledge with which your application is concerned. Don't use generic programmy words.

Really, though, how you pick conventions is up to you (and maybe slightly influenced by the framework you use.) But remember you should choose them early and pre-emptively: and document them! Even if it's only ever you working on the codebase, it's quite easy to forget some subtle decision you made six months ago about hyphens...!

Summary

Dependency injection is an application detail, and can involve a lot of boilerplate code. A trait for every injected service can reduce the repetition of boilerplate across the application, using traits how were originally designed: as a way to avoid copying and pasting!

Add new comment