Categories


Archives


Recent Posts


Categories


Magento 2’s Automatic Dependency Injection

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This week’s article is the second in a series discussing Magento 2’s object system. Last time we covered the basics of Magento’s object manager. This week we’re going explain why you’ll rarely use Magento’s object manager, as well as Magento’s implementation of the popular “dependency injection” pattern. (hint — they’re related)

Many popular PHP frameworks implement a dependency injection system — although it would be more accurate to say they implement an automatic dependency injection system. Before we dive into Magento’s system, we’ll want to explain the problem dependency injection sets out to solve.

Understanding Dependency Injection

The easiest way to understand dependency injection is by example. Consider the following PHP method/pseudo-code

//Obviously contrived -- if only getting a price were this simple
public function getFormattedPrice($sku)
{
    $db  = new DBHandler;
    $row = $db->query('SELECT price FROM products WHERE sku = ?', $sku);

    $formatter = new PriceFormatter;

    return $formatter->asDollars($row['price']);
}

This is a simplified example of what a method for fetching a product’s price might look like. On the surface, there’s nothing wrong with this method. It

The problem comes later, when someone else wants to reuse this method. This method is now dependent on the specific DBHandler class, and the specific PriceFormatter class. Even if you think code reuse is an untrue industry myth, these two dependencies make this method harder to test in an automated framework. Your test framework is now reliant on making the same database connection as your real application.

The solution to this problems is to not have methods with these sorts of dependencies. Instead, you should inject dependencies into the method.

public function getFormattedPrice($sku, $db, $formatter)
{
    $row = $db->query('SELECT price FROM products WHERE sku = ?', $sku);
    return $formatter->asDollars($row['price']);
}

Rewritten, this method has two new parameters. The idea is the client programmer should pass in instantiated objects (i.e. inject the dependencies).

That’s all dependency injection is — the wikipedia entry has more examples of the concept and is worth a read, if you can stomach the java examples!

Modern Dependency Injection

Dependency injection is a pretty simple concept, but if you’ve never encountered it before there may be some doubts scratching at the back of your head. One of them may be this

public function prepareDataForDisplay()
{
    //...

    $data             = new stdClass;
    $db              = new DBHandler;
    $formatter         = new PriceFormatter;
    $data['price']     = $this->getFormattedPrice($row['price']);

    //...
}

public function getFormattedPrice($sku, $db, $formatter)
{
    $row = $db->query('SELECT price FROM products WHERE sku = ?', $sku);
    return $formatter->asDollars($row['price']);
}

While we’ve replaced the dependencies in getFormattedPrice — all we’ve really done is shift them up a level to the method that calls getFormattedPrice (prepareDataForDisplay above). So, conceptually dependency injection is simple, but where you inject your dependencies and where these objects are instantiated is left up in the air.

This is the problem that many PHP frameworks try to solve with some sort of automatic dependency injection. By creating a system for automatically injecting these sorts of dependencies the framework removes the question of by who and how a dependency is injected. If that didn’t make sense, don’t worry. After looking at an example of Magento’s dependency injection it should start to make more sense.

Magento Dependency Injection

Just like we did last time, we’ve prepared a module with some sample code. The module’s on GitHub, and the easiest way to install it is to manually download the latest release.

If you need help manually installing a Magento extension our previous series article has complete instructions.

Let’s make sure you have the module installed correctly by running the following command

$ php bin/magento ps:tutorial-object-manager-2
Hello Again World!

If you see the “Hello Again World!” message, you’re good to go.

If we take a look at the class that implements our command, we see the following .

#File: app/code/Pulsestorm/TutorialObjectManager2/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    $manager = $this->getObjectManager();
    $helper = $this->getObjectManager()->create(
        '\Pulsestorm\TutorialObjectManager2\Model\Example');
    $output->writeln(
        $helper->sendHelloAgainMessage()
    );        
}

All we’ve done in the execute method is instantiate a Pulsestorm\TutorialObjectManager2\Model\Example object and call its sendHelloAgainMessage method to get some output text. If you take a look at this Pulsestorm\TutorialObjectManager2\Model\Example class’s constructor

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php    
public function __construct()
{
    $object = new Message; //
    $this->messageObject = $object;
}

we see the class instantiates a Pulsestorm\TutorialObjectManager2\Model\Message object and assigned it to the messageObject property. We use the short class name Message since this file lives in the Pulsestorm\TutorialObjectManager2\Model namespace. If you need some help getting started with PHP namespaces, our primer is a good place to go.

Then, in the sendHelloAgainMessage method

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php
public function sendHelloAgainMessage()
{
    return $this->messageObject->getMessage();
}

we use the Message object to return a text message.

Our (contrived) Pulsestorm\TutorialObjectManager2\Model\Example class has a hard coded dependency on the Pulsestorm\TutorialObjectManager2\Model\Message object. Here’s how Magento 2 solves this problem. Open up the Example.php class definition file, and let’s replace the constructor with the following code

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php
public function __construct(Message $message)
{
    $this->messageObject = $message;
}

What we’ve done here is added a parameter named $message to the constructor, and then assigned that parameter to the messageObject property. In other words, we’ve replaced the hard coded dependency with a parameter. This allows developers to inject the dependency. You’ll also notice we’ve included a type hint with the parameter.

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php
__construct(Message $message);

This type hint forces developers to pass in an object that either is a Pulsestorm\TutorialObjectManager2\Model\Message object, or has Pulsestorm\TutorialObjectManager2\Model\Message in its ancestry chain. Again, we’ve used the short class name Message. The following would have been equivalent, but is much more verbose.

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php
__construct(\Pulsestorm\TutorialObjectManager2\Model\Message $message);

If you’re not familiar with type hints, we’ve prepared a primer on them.

If this were a traditional PHP application, we’d inject the dependency with code that looks like this

$dependency = new \Pulsestorm\TutorialObjectManager2\Model\Message;
$helper        = new \Pulsestorm\TutorialObjectManager2\Model\Example($dependency);

However, with Magento 2’s object system, we don’t get to supply arguments

$helper = $this->getObjectManager()->create('Pulsestorm\TutorialObjectManager2\Model\Example');

So what are we supposed to do?

That’s the beauty of Magento 2’s automatic dependency injection system. You don’t need to do anything. Just run the command with our new code in place.

$ php bin/magento ps:tutorial-object-manager-2
Hello Again World!

Same results — even though we didn’t do anything to pass in a parameter.

This is automatic dependency injection. Behind the scenes, the object manager will use PHP’s reflection features to look at a class’s __construct type hints/parameters, automatically instantiate the object for us, and then pass it into the constructor as an argument.

That can seem a little weird the first time you encounter it, but if you don’t believe us just add a bit of debugging code to your constructor

#File: app/code/Pulsestorm/TutorialObjectManager2/Model/Example.php    
public function __construct(Message $message)
{
    var_dump(get_class($message));
    exit;
    $this->messageObject = $message;
}

re-run the command, and you’ll see the class name printed out.

$ php bin/magento ps:tutorial-object-manager-2
string(47) "Pulsestorm\TutorialObjectManager2\Model\Message"

Once you’re over the weirdness, you may wonder why this is any better. Isn’t the type hint just the hard coded dependency now?

The difference is the object manager has control over the instantiation of the dependency. As module developers, we get a great deal of power to change how the object manager instantiates an injected dependency. This happens via the module’s etc/di.xml file (the di stands for dependency injection). While Magento 2 doesn’t have “class rewrites” — it does have similar, more powerful features, all configurable via the di.xml file. Over the next few articles we’re going to explore all these options, and you’ll learn how to gain complete control over these dependencies.

So Long Object Manager, We Hardly Knew You

Before we wrap up for today, there’s one last thing to attend to. If you poke around Magento 2’s current documentation for Magento 2’s object system (usually labeled Dependency Injection), you’ll see cryptic comments like the following

The object manager must be present only when composing code, composing code is performed early in the bootstrapping process

Also, if you’ve been following Magento 2 development closely, you’ll know there used to be a static factory method for grabbing an object manager instance, but that method was depreciated and removed.

The object manager class is not meant for day-to-day use in Magento 2. It’s reserved (by convention) for system level developers working on the code that bootstraps Magento. The getObjectManager method we’ve provided in these tutorials was a helper so you could understand what the object manager was.

If you’re following best practices for Magento extension development, you’ll use dependency injection to instantiate almost all your objects. This may seem impossible at first (I was rolling my eyes back in 2013), but as Magento 2 nears completion it’s becoming clearer that this actually is possible.

We’ll be covering the dependency injection features that make this possible in our future articles. However, here’s some high level reassurances that “no objet manager” isn’t as crazy as it sounds.

Regarding that last item? Take a look at the base Pulsestorm\TutorialObjectManager2\Command\AbstractCommand class

#File: app/code/Pulsestorm/TutorialObjectManager2/Command/AbstractCommand.php
use \Magento\Framework\ObjectManagerInterface;

//...

public function __construct(ObjectManagerInterface $manager)
{
    $this->objectManager = $manager;
    parent::__construct();
}

protected function getObjectManager()
{
    return $this->objectManager;
}

This is the base class for our tutorial module’s cli classes, and where getObjectManager is implemented. How did we get an instance of the object manager? By using dependency injection, of course!

#File: app/code/Pulsestorm/TutorialObjectManager2/Command/AbstractCommand.php    
public function __construct(ObjectManagerInterface $manager)

This pattern can take a little getting used to, and may seem like overkill. However, in an enterprise environment consistency is more important than a home run. Constricting day-to-day development along this path and leaving more creative work to the deep level systems developers, an agile focused team can do a better job of hitting its deadlines and calculating its velocity. Boring and corporate as they are, these design patterns are an important part of ensuring Magento 2’s steady, relentless march towards a release.

That’s a lot to digest for a single day, so we’ll leave it there. Next time we’ll explore some of the dependency injection features, including how we managed to inject an object manager using only a PHP interface (ObjectManagerInterface) instead of a class.

Originally published July 13, 2015
Series Navigation<< Magento 2 Object ManagerMagento 2 Object Manager Preferences >>