Why I Prefer Constructor Injection over Setter or Property Injection

Magento 2 uses constructor injection as a part of the move towards dependency injection. This is a quick post on why I personally believe constructor injection is the best approach.

And to give appropriate credit, it was the core team and individuals like Anton Kril that drove the introduction of dependency injection in Magento. I just agree they made the right choices.

What is Dependency Injection?

There are some good write ups around for those who want to dig deep, including on Wikipedia and on the Symfony site. I have written up on dependency injection before as well. Put simply, the idea of dependency injection is for classes to accept objects they need from the outside, rather than calling ‘new’ on a class directly. As soon as your code invokes ‘new’ on a class, it becomes tightly bound with that class. When the needed objects are injected (supplied from outside the class), a subclass can be swapped in instead of the original class or an interface.

There are a number of benefits of dependency injection, including:

  • Unit tests are frequently easier to write by replacing real classes with mock implementations.
  • Looser coupling means the code base becomes more extensible and modular, which is important for Magento.
  • Code written to be compatible with dependency injection works even if a dependency injection framework is not used. You can replace the dependency injection framework or even drop it completely without changing a single line of application code (unless that code decides to interacts with the dependency injection framework directly, which is discouraged).

Constructor Injection

Magento 2 supports constructor injection. This is where the constructor is provided in constructor parameters all the external resources the class needs. The constructor has one parameter per external object to be provided which it then saves away in properties for usage later by methods of the class.

class MyClass {

    /** @var string */
    private $property1;

    /** @var OtherClass */
    private $property2;

    /**
     * Constructor.
     * @param string $property1
     * @param OtherClass $property2
     */
    public function __construct(
        $property1,
        OtherClass $property2
    ) {
        $this->property1 = $property1;
        $this->property2 = $property2;
    }

    ... rest of class ...
}

The biggest negative of constructor injection is the verbosity of code. You need to declare a parameter per external object in the constructor, a PHP Doc @param for each parameter, a property (with PHP Doc) for each parameter, and you need to copy each parameter to each property.

While verbosity is a negative, the other approaches I believe are worse, as I describe below.

Setter Injection

Rather than having a long parameter list, another approach is to have a ‘setter’ method per value to be injected. Each setter copies the function parameter to the property. The constructor then has a much shorter argument list.

class MyClass {

    /** @var string */
    private $property1;

    /** @var OtherClass */
    private $property2;

    /**
     * Constructor.
     */
    public function __construct() {
    }

    /**
     * Set property 1.
     * @param string $property1
     */
    public function setProperty1($property1) {
        $this->property1 = $property1;
    }

    /**
     * Set property 2.
     * @param OtherClass $property2
     */
    public function setProperty2(OtherClass $property2) {
        $this->property2 = $property2;
    }

    ... rest of class ...
}

Reasons I dislike setter injection include:

  • The object is not fully constructed and usable when the constructor function returns. This feels “wrong”. I prefer the object being completely usable when the constructor returns.
  • If new properties are added to an existing class, setter injection does not guarantee that all will be found and set. New properties may be missed. Constructor injection will ensure all properties are always set.
  • Setters encourage objects to be changed after creation. I prefer objects to minimize change over time.
  • Setter methods may be intended to exist only for the dependency injection framework to use – but it is hard to document / control this.
  • The code can be even more verbose than with constructor injection due to having lots of small setter methods. “Magic methods” can reduce boilerplate text, but introduce their own problems.

Property Injection

Property Injection is another approach where annotations are used on the properties to identify them to the dependency injection framework to populate. (I made up an “@autowired” annotation for the sake of this example – it does not really exist.)

class MyClass {

    /** @var string @autowired */
    private $property1;

    /** @var OtherClass @autowired */
    private $property2;

    /**
     * Constructor.
     */
    public function __construct() {
    }

    ... rest of class ...
}

The benefits include significantly less boilerplate text – there are no constructor arguments, not setters. Only properties need to be declared. This is pretty clean in terms of code readability.

The negatives include:

  • The code is now dependent on the existence of a dependency injection framework. There is no way to just call the class constructor and have it set up.
  • Properties can only be injected after the constructor has been called. This means the constructor code cannot safely use the properties, sometimes resulting in a second method call needing to be made to finish the construction flow.

While there is a lot less code, the drawbacks are not acceptable to me. (But for a class with lots of parameters for the constructor I must admitting to being sorely tempted at times.)

Is There No Other Way?

Is there any way this could be made nicer? In PHP, not that I know of, but I am not a PHP expert.

One language that shows how nice it could be however is Scala. When you declare a constructor in Scala the parameters automatically become properties that any function in the class can access at any time. It is like the parameters are automatically copied into properties for you behind the scenes. The properties are also read-only, so you cannot accidentally change them. Making up possible syntax for PHP 29, this might look something like:

class MyClass {

    /**
     * Constructor.
     * @param string $property1
     * @param OtherClass $property2
     */
    public function __construct(
        private property string $property1,
        private property OtherClass $property2) {
    }

    ... rest of class ...
}

Another approach is to supply the dependency injection arguments in a single array. This would reduce the number of constructor and @param declarations, but would still require properties to be declared and the constructor arguments copied into. You could save the whole array as a single property, but I personally like having separate properties with types, allowing IDEs to do things like autocompletion.

Conclusions

In conclusion, out of the choices of constructor, setter, or property injection, I think constructor injection is the best approach. The extra text required *is* annoying, but it is better than the other options (at least until PHP 29 comes along – and yes, that is a joke).

It should also be said that a class with a large number of constructor parameters may have other issues than just the length of the constructor parameter list. How many other classes should one class depend on to be considered well designed?

Disclaimer: This post is my personal opinions and not that of my employer.

7 comments

  1. I agree on all points and want to add: the main reason that it feels overly verbose in Magento 2 is the dependency hell where constructors have more than 5 arguments. And it’s a good thing that this looks bad because it encourages the necessary refactoring.

  2. Thanks for the article, although I think you did not explain why Magento 2 uses constructor injection. You mostly mentioned some weak points of some other approaches (with which I mostly disagree but that’s another topic).

    I would like to ask why didn’t you choose to inject container instead of using constructor arguments. I think it’s a better approach and fixes the verbosity you mentioned. Many applications based on Symfony2 use that a lot.

    1. If you want some deep reading, try http://martinfowler.com/articles/injection.html. A lot more depth than my quick post here.

      When you say “container injection”, I assume you mean http://symfony.com/doc/current/components/dependency_injection/introduction.html? I am not a Symfony expert, but this looks like the service locator pattern. In Magento 2 its called the ObjectManager. You can write code that depends on ObjectManager and look up classes that way. But this makes your code dependent on the ObjectManager and our specific implementation of the dependency injection framework. (The Symfony page also discourages overuse of this pattern – “Whilst you can retrieve services from the container directly it is best to minimize this.”)

      It was a very conscious decision to move away from the service locator pattern where possible as it hides all the dependencies. They are present, but hidden buried away in the code. Magento 2 tries not to bring such dependencies to the surface, hopefully resulting in more being eliminated.

      Oh, and this article is definitely a personal view of programming piece. I have a multi-threaded programming background which probably makes me even more allergic than normal to mutable classes and the locking issues they create. So I like “once object is created it is ready to go”. Maybe less relevant to PHP, but a definite personal bias.

  3. Thanks for the quick reply. Yeah maybe I didn’t express myself correctly, my idea was just to replace all the arguments in constructor with one container object that would contain all the dependencies. ObjectManager in Magento2 is something different, it is a global object that is accessible everywhere in the code, the container would only be passed to constructor. In di.xml you could see all the dependencies (no need to declare them twice, in di.xml and in constructor signature).

    I agree that it has some drawbacks with class being tightly coupled to container being the biggest one. One drawback also is that with this approach classes become slightly less transparent about their dependencies.

    By the way, some other ecommerce projects use this, such as Sylius: https://github.com/Sylius/Sylius/blob/d708e6c076af2adedb10c9d89e87517849f1c769/src/Sylius/Bundle/CoreBundle/Routing/RouteProvider.php

    1. Context objects are present in some of Magento 2 abstract classes (Block, Model, Controller). Their goal is to make post-release base class modifications possible (non-conflicting). But they make it harder to configure specific dependencies for that classes.

  4. Same opinion here.
    Just one wonder, wouldn’t be good specifying default dependency:

    class MyClass {

    /** @var string */
    private $property1;

    /** @var OtherClass */
    private $property2;

    /**
    * Constructor.
    * @param string $property1
    * @param OtherClass $property2
    */
    public function __construct(
    $property1 = null,
    OtherClass $property2 = null
    ) {
    $this->property1 = $property1 === null ? new Mage_Default_Property1() : $property1;
    $this->property2 = $property2 === null ? new Mage_Default_Property2() : $property2;
    }

    … rest of class …
    }

    // Now we can just simply call
    new MyClass();

    // or
    new MyClass(new Third_Party_Property());

    // or
    new MyClass(null, new Third_Party_Property2());

    This way when we want to free from some dependency, we just have to parse our variable/object.

  5. I love dealing with dependencies and your summary is so nicely written. Thank you for sharing this!

    I’m actually working on container, which would only support constructor injection.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: