Categories


Archives


Recent Posts


Categories


Magento 2 Object Manager Preferences

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!

Today we’re going to explore the “class preference” feature of Magento 2’s object-manager/dependency-injection system. While this feature is a direct descendant of Magento 1’s class rewrite system, it ends up playing a different role in Magento 2. Where Magento 1’s class rewrites were aimed at allowing third party developers to customize system behavior, the class preference system is a core Magento 2 feature used to enforce a design by interface contract style of systems programming.

Installing the Sample Module

Like our other tutorials, we’ve prepared a module that sets up the basic commands and classes we use in this article. You can find the module on GitHub. As of this writing, the installation procedure for a Magento module isn’t 100% clear, so we recommend installing these tutorial modules manually using the latest tagged release. If you need help installing a Magento module manually, the first article in this series contains detailed instructions for doing so.

You’ll know you have the module installed correctly when you get the following output when running the ps:tutorial-object-preference command.

$ php bin/magento ps:tutorial-object-preference
Hello!

Assuming you can run the ps:tutorial-object-preference command, we’re ready to start.

Reviewing the Command Implementation

First, let’s review the implementation of the ps:tutorial-object-preference command in the following class file

#File: app/code/Pulsestorm/TutorialObjectPreference/Command/Testbed.php
<?php
namespace Pulsestorm\TutorialObjectPreference\Command;

use Magento\Framework\ObjectManagerInterface;
use Pulsestorm\TutorialObjectPreference\Model\Messenger;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Testbed extends Command
{
    protected $object_manager;
    protected $messenger;

    public function __construct(Messenger $messenger, ObjectManagerInterface $om)
    {
        $this->object_manager = $om;
        $this->messenger      = $messenger;

        return parent::__construct();
    }

    protected function configure()
    {
        $this->setName("ps:tutorial-object-preference");
        $this->setDescription("A command the programmer was too lazy to enter a description for.");
        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln(
            $this->messenger->getMessage()
        );  
    }
}        

If you’ve been following along with our series, nothing in this file should surprise you.

If any of the above confused you, you may want to review the previous articles in this series.

Next, let’s take a look at the Pulsestorm\TutorialObjectPreference\Model\Messenger class definition file.

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Messenger.php 
<?php
namespace Pulsestorm\TutorialObjectPreference\Model;
class Messenger
{
    protected $message_holder;
    public function __construct(MessageHolderInterface $mhi)
    {
        $this->message_holder = $mhi;
    }

    public function getMessage()
    {
        return $this->message_holder->getHelloMessage();
    }
}

Here we see a similar pattern — the getMessage method calls the getHelloMessage method of the object in the message_holder property. The message_holder object contains an object that Magento creates via the __construct dependency injection system.

There is something different this time. The “object” that Magento injects is a Pulsestorm\TutorialObjectPreference\Model\MessageHolderInterface. Take a look at MessageHolderInterface‘s definition file

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/MessageHolderInterface.php 
<?php
namespace Pulsestorm\TutorialObjectPreference\Model;
interface MessageHolderInterface
{
    public function getHelloMessage();
}

That’s no class, its a space station, PHP interface! If you’re not familiar with interfaces, we’ve written a short primer — but experienced PHP developers will know why we’re using exclamation points. You can’t instantiate a PHP interface — they’re not classes. So how is Magento injecting an interface?

Based on what we’ve learned about Magento’s object manager so far, you’re right to be confused. However, this is where the class preference feature comes into play.

Configuring Preferences for Classes

The first response to confusing code? Debugging with PHP’s reflection features, of course. Let’s try adding the following temporary var_dump debugging code to the Pulsestorm/TutorialObjectPreference/Model/Messenger‘s constructor.

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Messenger.php 
<?php
    public function __construct(MessageHolderInterface $mhi)
    {
        var_dump(
            get_class($mhi)
        );
        exit;
        $this->message_holder = $mhi;
    }

Here we’re using PHP’s get_class function to view the object Magento injects. If we run our command with the above in place, we’ll see the following output.

$ php bin/magento ps:tutorial-object-preference
string(49) "Pulsestorm\TutorialObjectPreference\Model\English"

So, we know that Magento injects a Pulsestorm\TutorialObjectPreference\Model\English, and that there’s not some unholy voodoo that allows us to actually instantiate an interface, (phew!) However, this does raise another question

Why is the object manager (via dependency injection), instantiating a Pulsestorm\TutorialObjectPreference\Model\English object when the type hint is for a Pulsestorm\TutorialObjectPreference\Model\MessageHolderInterface?

This is what the class preference system is — it’s a way for end users to configure which classes Magento’s object manager should actually use when the object manager requests a certain class/type (or, in the context of automatic dependency injection, encounters a specific type hint).

In the next section we’ll cover how to configure a class preference, but don’t forget to remove the temporary var_dump and exit above before we move on.

Configuring Class Preferences

Most (if not all?) of Magento’s object manager features are configured through a module’s etc/di.xml file. For a Magento 1 developer coming in new to Magento 2, the new system continues the work that was going on in later versions of Magento 1, and splits out feature specific configuration information into their own configuration files.

If you take a look at di.xml, you’ll see the following “top level” (under the real top level <config/>, that is) node.

<!-- File: app/code/Pulsestorm/TutorialObjectPreference/etc/di.xml -->
<preference 
    for="Pulsestorm\TutorialObjectPreference\Model\MessageHolderInterface" 
    type="Pulsestorm\TutorialObjectPreference\Model\English" />

This is the configuration node where we can set a class preference. In plain english, we’re telling the object manager that

When someone asks you to instantiate a Pulsestorm\TutorialObjectPreference\Model\MessageHolderInterface (the for attribute) you should actually instantiate a Pulsestorm\TutorialObjectPreference\Model\English object (the type attribute)

If we take a look at the definition for Pulsestorm\TutorialObjectPreference\Model\English, we’ll see our simple Hello message

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/English.php    
//...
public function getHelloMessage()
{
    return 'Hello!';
}
//...

If you’re having trouble believing this, a quick example should set you straight. Let’s edit di.xml so it matches the following

<!-- File: app/code/Pulsestorm/TutorialObjectPreference/etc/di.xml -->
<preference 
    for="Pulsestorm\TutorialObjectPreference\Model\MessageHolderInterface" 
    type="Pulsestorm\TutorialObjectPreference\Model\Spanish" />

Here we’ve changed the type attribute to Pulsestorm\TutorialObjectPreference\Model\Spanish. This is a pre-defined class we included with the module. If we clear our Magento cache

$ php bin/magento cache:clean 
Cleaned cache types:
config
layout
block_html
view_files_fallback
view_files_preprocessing
collections
db_ddl
eav
full_page
translate
config_integration
config_integration_api
config_webservice

and re-run our command, we should see the following changed output

$ php bin/magento ps:tutorial-object-preference
Hola

The Hola text comes from our newly configured class

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Spanish.php
public function getHelloMessage()
{
    return 'Hola';
}        

By changing di.xml, we told the object manager to inject a different class/object. While this example is (obviously) simplified for pedagogical reasons, this is a powerful feature you can use to change the behavior of Magento 2 system wide.

Not Just Interfaces

Class preference configuration is not just for interfaces — you can change actual classes as well. Let’s jump back to our command definition file

#File: app/code/Pulsestorm/TutorialObjectPreference/Command/Testbed.php
use Pulsestorm\TutorialObjectPreference\Model\Messenger;
//...
public function __construct(Messenger $messenger, ObjectManagerInterface $om)
{
    $this->object_manager = $om;
    $this->messenger      = $messenger;

    return parent::__construct();
}

You’ll recall Magento instantiates the Pulsestorm\TutorialObjectPreference\Model\Messenger object via automatic constructor dependency injection. Even though this is a concrete class, we can still change it. Try adding the following new <preference/> node to di.xml

<!-- File: app/code/Pulsestorm/TutorialObjectPreference/etc/di.xml -->
<config>
    <!-- ... -->
    <preference 
        for="Pulsestorm\TutorialObjectPreference\Model\Messenger" 
        type="Pulsestorm\TutorialObjectPreference\Model\Messenger2" />
    <!-- ... -->
</config>

Here we’ve told Magento that anytime someone requests that the object manager instantiate a Pulsestorm\TutorialObjectPreference\Model\Messenger object, that the object manager should actually instantiate a Pulsestorm\TutorialObjectPreference\Model\Messenger2 object.

If you add the above node to di.xml, clear your cache, and re-run the program, you should see the following output

$ php bin/magento ps:tutorial-object-preference
Injection?  We don't need no stinking injection!

That less than friendly message comes directly from our newly configured Pulsestorm\TutorialObjectPreference\Model\Messenger2 class.

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Messenger2.php
<?php
namespace Pulsestorm\TutorialObjectPreference\Model;
class Messenger2 extends Messenger
{    
    public function getMessage()
    {
        return 'Injection?  We don\'t need no stinking injection!';
    }
}

This, in effect, makes the class preference system the successor to Magento 1’s class rewrite system.

Enforcing “Design by Contract”

While the class preference system is similar to Magento 1’s class rewrite system, it’s superior in at least one way. Let’s run through one last example.

Consider the following Pulsestorm\TutorialObjectPreference\Model\Messenger3 class

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Messenger3.php
<?php
namespace Pulsestorm\TutorialObjectPreference\Model;
class Messenger3
{    
    public function getMessage()
    {
        return 'Injection?  We don\'t need no stinking injection!';
    }
}

This class is very similar to our Pulsestorm\TutorialObjectPreference\Model\Messsage2 class, with one exception: It doesn’t extend the original Pulsestorm\TutorialObjectPreference\Model\Message class. Let’s see what happens when we configure this class in di.xml (replacing the Message2 class)

<!-- File: app/code/Pulsestorm/TutorialObjectPreference/etc/di.xml -->
<config>
    <!-- ... -->
    <preference 
        for="Pulsestorm\TutorialObjectPreference\Model\Messenger" 
        type="Pulsestorm\TutorialObjectPreference\Model\Messsage3" />
    <!-- ... -->
</config>

Clear your cache and run the command —

$ php bin/magento ps:tutorial-object-preference
Autoload error: Argument 1 passed to
Pulsestorm\TutorialObjectPreference\Command\Testbed::__construct() 
must be an instance of Pulsestorm\TutorialObjectPreference\Model\Messenger
instance of Pulsestorm\TutorialObjectPreference\Model\Messenger3 given,
called in
lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php   
on line 99 and defined ...

Curses — an error! However, this is a helpful error. What happened here was we successfully configured Messenger3 — but when Magento tried to inject it into the __construct method

#File: app/code/Pulsestorm/TutorialObjectPreference/Model/Messenger3.php

public function __construct(Messenger $messenger, ObjectManagerInterface $om)

this new class failed the type hint check, and PHP rejected the change. While this may seem annoying, it’s actually a good thing. The class preference system in Magento 2 has an added layer of type safety. In Magento 1 you could replace a class alias like Mage::getMode(catalog/product) with any PHP class you wanted — but if that class was missing a method, your project would fail, sometimes in mysterious “not immediately evident” ways.

Similarly, our previous English and Spanish classes each implemented the MessageHolderInterface

namespace Pulsestorm\TutorialObjectPreference\Model;
class English implements MessageHolderInterface

namespace Pulsestorm\TutorialObjectPreference\Model;
class Spanish implements MessageHolderInterface

By using PHP’s built-in type hints for automatic dependency injection, Magento gets this added level of type safety for free. By forcing developers along a path where they’re required to use dependency injection to instantiate objects, Magento forces developers along this type-safe (or type-less-dangerous) path.

Once you’ve digested all this, you may be left with a lingering question

Is it better to use interfaces for all my injected dependencies, or should some be concrete class files?

While that’s a very good question, it’s a question without (at the moment) a clear answer. I will say, all other things being equal, it would be better to have an interface for any dependencies you’re injecting into the system, as an interface allows future developers more flexibility in building a replacement class for your dependency. Of course all things are never equal, and the true best practice will reveal itself after Magento 2 starts getting some real world use.

To Rewrite or Not Rewrite

While we’ve described the object manager’s class preference system as a replacement for the class rewrite system — and you can certainly use it as such — its real value comes from being a tool for developers interested in using design by contract style programming in their modules. By letting developers specify dependency injectable type hints that are interfaces, developers are (in turn) encouraged to use interfaces — knowing they can swap out their concrete implementations if they need to with a simple configuration change (as opposed to a complex refactoring).

There’s also another reason to look askance at the class preference feature as a replacement for class rewrites — and that’s because Magento’s object manager comes with a slew of other features that are both more powerful, and more specific, than the broad rewrite concept. We’ll explore more of these features in our next article, when we cover the configurable arguments feature of Magento 2’s object manager system.

Originally published July 22, 2015
Series Navigation<< Magento 2’s Automatic Dependency InjectionMagento 2 Object Manager Argument Replacement >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 22nd July 2015