Magento 2 Object Manager Virtual Types

Last time we discussed the argument replacement feature of Magento 2’s object manager, and introduced the <type/> tag in di.xml. This week we’re going to talk about another feature related to argument replacement — Virtual Types.

Virtual types allow a module developer (you!) to create “configuration only classes” for the object manager’s argument replacement feature. If that didn’t make sense, don’t worry, by the end of this article you’ll understand how to read and trace <virtualType/> configurations, and have the information you’ll need to decide if they’re for you.

Warning: This article assumes you’ve been following along in our series, and have a general understanding of Magento’s automatic dependency injection feature, argument replacement, and the role of Magento’s object manager. This article makes direct use of the object manager to simplify concepts. In the real world Magento’s best practices dictate end-user-programmers (you!) not use the object manager directly. Instead you should rely on automatic constructor dependency injection for instantiating your objects. All the features we discuss will be available to objects created via automatic constructor dependency injection.

Additional Warning: Despite our attempts to simplify things, virtual types involve nested levels of automatic constructor dependency injection — if you’re having trouble grasping the concepts it’s not because you’re not smart, it’s because they’re complicated concepts and take time to understand.

Installing the Module

We’ve created a module with much of the boiler plate code you’ll need to get started with virtual types. The module is on GitHub. The official installation procedure for a Magento module is still being worked out, so we recommend installing these tutorial modules manually using the latest tagged release. If you’re not sure how to install a module manually, the first article in this series has the instructions you’re looking for.

To test that you’ve installed the module correctly, try running the following command

$ php bin/magento ps:tutorial-virtual-type
Installed Pulsestorm_TutorialVirtualType!

Once you see the Installed Pulsestorm_TutorialVirtualType! output, you’re ready to start.

The Object Setup

This module’s main purpose is to setup a few object relationships. At the top of our composition hierarchy, we have a Pulsestorm\TutorialVirtualType\Model\Example object. This object’s class makes use of automatic constructor dependency injection

#File: app/code/Pulsestorm/TutorialVirtualType/Model/Example.php
<?php
namespace Pulsestorm\TutorialVirtualType\Model;
class Example
{
    public $property_of_example_object;
    public function __construct(Argument1 $the_object)
    {
        $this->property_of_example_object = $the_object;
    }
}

The automatic constructor dependency injection will inject a Pulsestorm\TutorialVirtualType\Model\Argument1 object, which is then assigned to the property named property_of_example_object.

In turn, this Pulsestorm\TutorialVirtualType\Model\Argument1 class also uses automatic constructor dependency injection.

#File: app/code/Pulsestorm/TutorialVirtualType/Model/Argument1.php
<?php
namespace Pulsestorm\TutorialVirtualType\Model;
class Argument1
{
    public $property_of_argument1_object;
    public function __construct(Argument2 $the_argument)
    {
        $this->property_of_argument1_object = $the_argument;
    }
}

This time the object manager will inject a Pulsestorm\TutorialVirtualType\Model\Argument2 object, which the constructor assigns to the object property named property_of_argument1_object.

In hierarchical terms, we have (using shorthand class names)

Example (contains a)
    Argument1 (contains a)
        Argument2

So far, all this is standard issue automatic dependency injection. If there’s a concept that’s confusing, you may want to review the series so far before asking questions in the comments or on Stack Overflow.

Reporting Command

Similar to our argument replacement article, we’ve prepared a simple program that reports on the above object hierarchy in real time using some of PHP’s reflection features. Open up the command class and find the execute method

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed Pulsestorm_TutorialVirtualType!");  
    //$this->showNestedPropertiesForObject();
}

Comment out the writeln line, and uncomment the call to showNestedPropertiesForObject

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed Pulsestorm_TutorialVirtualType!");  
    //$this->showNestedPropertiesForObject();
}

If you run the command with the above in place, you’ll see the following output.

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

We’re not going to go too in-depth into how this reporting works, but if you look at the showNestedPropertiesForObject method.

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function showNestedPropertiesForObject()
{
    $object_manager = $this->getObjectManager();        
    $example         = $object_manager->create('Pulsestorm\TutorialVirtualType\Model\Example');
    $this->output("First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object");        
    $properties     = get_object_vars($example);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }  

    $this->output("Next, we're going to report on the Example object's one property (an Argument1 class)");  
    $properties     = get_object_vars($example->property_of_example_object);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }  

    $this->output("Finally, we'll report on an Argument1 object, instantiated separate from Example");          
    $argument1  = $object_manager->create('Pulsestorm\TutorialVirtualType\Model\Argument1');
    $properties = get_object_vars($argument1);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }          
}                     

You’ll see the program

  1. Instantiates a Pulsestorm\TutorialVirtualType\Model\Example object via the object manager, and then reports on it

  2. Fetches the Example object’s property_of_example_object property and reports on the object inside of it (a Pulsestorm\TutorialVirtualType\Model\Argument1 object)

  3. Finally, we use the object manager to directly instantiate a Pulsestorm\TutorialVirtualType\Model\Argument1 object, and report on it.

We’ll use this reporting in the next section to analyze how our dependency injection configuration changes the system’s behavior.

Creating a Virtual Type

The stage is set. We’re ready to create our virtual type. As we mentioned in our introduction, creating a virtual type is sort of like creating a sub-class for an existing class

<?php
class OurVirtualTypeName extends \Pulsestorm\TutorialVirtualType\Model\Argument1
{
}

Except, we’re not doing it in code. To create a virtual type in Magento 2, just add the following configuration to the module’s di.xml

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      
</config>

The <virtualType/> nodes live directly under the main <config/> node. They have two attributes (name and type). The name attribute defines the name of our virtual type — this should be a globally unique identifier, similar to a class name. The type attribute is the real PHP class our virtual type is based on.

That’s all there is to defining a virtual type. Of course — simply defining a virtual type will have no effect on system behavior. If you clear your cache and re-run the command, the output will be exactly the same.

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Using the Virtual Type

The purpose of a virtual type is to take the place of PHP classes in argument replacement. For example, without virtual types, if we wanted to change the argument injected into the Example class’s constructor, we’d add a <type/> configuration something like this.

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml    
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">Some\Other\Class</argument>
        </arguments>
    </type>        

</config>

Of course, if you tried running the command with the above configuration in place (after clearing your cache), you’d see an error like the following.

$ php bin/magento ps:tutorial-virtual-type

  [ReflectionException]                  
  Class Some\Other\Class does not exist  

That’s because the class we tried to inject (Some\Other\Class) is not defined. Let’s try changing our configuration to match the following

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml      
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">ourVirtualTypeName</argument>
        </arguments>
    </type>        

</config>

Here we’ve replaced Some\Other\Class with ourVirtualTypeName. You might expect the above configuration to also cause an error, (since there’s no class named ourVirtualTypeName), except if you run our command with the above in place —

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

— there’s no error! That’s because there is a “class” named ourVirtualTypeName. It’s just not a real PHP class — instead it’s our virtual type. At their most basic level, virtual types allow you to create what amounts to a class alias for a PHP class, and use that alias in your di.xml configuration.

That said — our output still looks the same. It turns out that it takes more than the simple creation and use of a virtual type to have a measurable impact on our system.

Changing Virtual Type Behavior

Earlier we said creating a virtual type was sort of like creating a sub-class of another class.

class OurVirtualTypeName extends \Pulsestorm\TutorialVirtualType\Model\Argument1
{
}

If we created a real sub-class of another class, we could change all sorts of things (method definitions, properties, constants, traits, etc) about that class.

With virtual types, the only behavior you can change in your virtual sub-class is which dependencies are injected. If that’s not clear, give the following configuration a try.

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml    
<config>
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
        <arguments>
            <argument name="the_argument" xsi:type="object">Pulsestorm\TutorialVirtualType\Model\Argument3</argument>
        </arguments>
    </virtualType>

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">ourVirtualTypeName</argument>
        </arguments>
    </type>  
</config>

This configuration is identical to our previous configuration with one exception — we’ve added argument sub-nodes to our virtual type. Same as they would under a <type/> node, the <arguments/>, <argument/> nodes under the <virtualType/> node replaces the argument with the name “the_argument” with a Pulsestorm\TutorialVirtualType\Model\Argument3 object. The argument nodes for a virtual type behave exactly as they would for a regular <type/> node — except they only effect the virtual type and not the original parent class.

If that was hard to follow (and it was), try clearing your cache and running the command again

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument3

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2     

This is the main selling point of virtual types. You’ll notice that the property_of_argument1_object property is now an Argument3 object — but only when that parameter’s owner class (Argument1) is instantiated by dependency injection in the Example class. When we instantiate Argument1 by itself — Magento does not inject a dependency.

Worth It?

On one hand, virtual types allow us even more specificity in argument replacement. Whereas regular argument replacement lets us effectively change the behavior of a class dependency when it’s used in a specific class — virtual types allow us to effectively change the behavior of a dependency when it’s used in a specific class — and when that specific class is, itself, used in a specific class. In theory, this is great, and offers the potential for greater system stability.

However, for day-to-day Magento development, I’m not sure virtual types will be worth the confusion. While there’s lots of programmers out there who can keep track of those three level deep dependencies in their head, in my own limited interactions with virtual types, I’ve had a hard time keeping track of what configuration injects what dependency, while also keeping track of the problem at hand.

Beyond being, perhaps, an abstraction too far, there’s another sort of confusion virtual types introduce — consider this di.xml configuration from the core code.

#File: app/code/Magento/Catalog/etc/di.xml
<type name="Magento\Catalog\Model\Session">
    <arguments>
        <argument name="storage" xsi:type="object">Magento\Catalog\Model\Session\Storage</argument>
    </arguments>
</type>

This appears to be a straight forward configuration for automatic constructor dependency injection — Magento will replace the storage constructor argument in Magento\Catalog\Model\Session with the PHP class Magento\Catalog\Model\Session\Storage.

Except — there is no PHP class Magento\Catalog\Model\Session\Storage.

If you look in the same di.xml file, you’ll see the following.

#File: app/code/Magento/Catalog/etc/di.xml 
<virtualType name="Magento\Catalog\Model\Session\Storage" type="Magento\Framework\Session\Storage">
    <arguments>
        <argument name="namespace" xsi:type="string">catalog</argument>
    </arguments>
</virtualType>

It turns out the Magento 2 core team has created virtual types with names that look like real PHP class names. While this helps ensure the names are globally unique — it can create confusion for developers who aren’t aware Magento\Catalog\Model\Session\Storage is a virtual type — especially developers who are still learning the ins and outs of Magento’s object manager system and class autoloading.

All in all, while I can see why the team responsible for creating Magento 2 might find value in virtual types — I’m not sure they’re the best choice for developers creating Magento stores and extensions — especially when there’s a much more powerful, and controllable means for extending Magento system behavior in the plugin system. This plugin system will be our final stop in the Magento object manager tutorial.

However, before we can get there, there’s a few loose ends to tie up. Next week we’ll be covering instance vs. singleton objects with dependency injection, creating non-injectable instance objects using factories, as well as Magento 2’s proxy objects.

Originally published August 2, 2015

Magento 2 Object Manager Argument Replacement

In the last article of our series, we covered the “class preference” feature of Magento’s object manager. This week we’ll cover a similar, but more selective, class replacement system built into Magento 2. While this article should be accesible as a stand-alone tutorial for programmers familiar with modern object oriented concepts, if you’re having trouble try starting at the beginning.

Installing the Module

Like our other articles, we’ve prepared a module that sets up the basic commands and classes we’re going to use. 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-manager-arguments
Installed!

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

Constructor Arguments

We’re going to need a little more exposition before we can get to the meat of this article. The exposition should also serve as a good review of some basic Magento 2 concepts.

With the command installed, open up its definition file and examine the execute method

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed!");          
    //$this->showPropertiesForObject();
}

You’ll notice the writeln command, along side a commented out call to the command’s showPropertiesForObject method. Let’s change the command to call the showPropertiesForObject command instead of outputting the Installed! text

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    //$output->writeln("Installed!");          
    $this->showPropertiesForObject();
}

If you run the command now, you’ll see the following output

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

The Property $object2
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument2

The Property $scaler1
  is a string
  with a value of: foo

The Property $scaler2
  is an integer
  with a value of: 0

The Property $scaler3
  is a boolean
  with a value of: false

The Property $thearray
  is an array
  with the elements:
  0=>foo

All this is simple enough, although probably a little confusing given our complete lack of context. Let’s take a look at the definition of showPropertiesForObject.

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function showPropertiesForObject()
{
    $object_manager = $this->getObjectManager();        
    $object         = $object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');
    $properties     = get_object_vars($object);
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }
}

This method is pretty straight forward. We fetch an instance of the object manager and use it to instantiate an object from the Pulsestorm\TutorialObjectManagerArguments\Model\Example class.

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
$object_manager = $this->getObjectManager();        
$object         = $object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');

Then, using PHP’s built-in global get_object_vars function, we fetch an array of the Example object’s properties, and then for each of these we pass the property to the reportOnVariable method

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
$properties     = get_object_vars($object);
foreach($properties as $name=>$property)
{
    $this->reportOnVariable($name, $property);       
}

This reportOnVariable method produces the output we saw above.

The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

For each variable passed, the method will output the variable’s type, and then class and/or value (depending on the type). The implementation of reportOnVariable is beyond the scope of this article, but feel free to poke around if you’re feeling exploratory.

Going back to our command’s output

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

The Property $object2
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument2

The Property $scaler1
  is a string
  with a value of: foo

The Property $scaler2
  is an integer
  with a value of: 0

The Property $scaler3
  is a boolean
  with a value of: false

The Property $thearray
  is an array
  with the elements:
  0=>foo 

We see our Example object has six properties. A quick look at its definition file should reveal this to be true

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php
<?php    
namespace Pulsestorm\TutorialObjectManagerArguments\Model;
class Example
{
    public $object1;
    public $object2;
    public $scaler1;
    public $scaler2;
    public $scaler3;
    public $thearray;

    public function __construct(
        ExampleArgument1 $object1,
        ExampleArgument2 $object2,
        $scaler1='foo',
        $scaler2=0,
        $scaler3=false,
        $thearray=['foo'])        
    {
        $this->object1 = $object1;
        $this->object2 = $object2;    

        $this->scaler1 = $scaler1;
        $this->scaler2 = $scaler2;
        $this->scaler3 = $scaler3;        
        $this->thearray   = $thearray;                
    }
}  

Here we’re dealing with a pretty standard issue Magento 2 class file. Six variables are included in the constructor. Magento 2’s automatic dependency injection automatically injects the two objects

Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1
Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

and the remaining parameters are either plain old PHP scalers, or the final argument, which is a PHP array.

Regular arguments may coexist with PHP dependency injection — if you’re wondering why this would be of any use, hold on for a few more paragraphs.

Object Manager Reminder

With that lengthy exposition out of the way we’re almost ready to start talking about today’s feature: Argument Replacement. Argument replacement is another feature that, while labeled as part of “dependency injection”, is really (from another point of view), part of the underlying object manager system.

You’ll remember from previous tutorials that for most of your day-to-day Magento programming, you won’t instantiate an object directly with the object manager as we have

$object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');

Instead, you’d inject your object in the __construct method of whatever class file your’e working on

public function __constrcut(
    \Pulsestorm\TutorialObjectManagerArguments\Model\Example $example        
);

With the above in place Magento, behind the scenes, will use the object manager to create the Pulsestorm\TutorialObjectManagerArguments\Model\Example object. We’re using the object manager in these tutorials for simplicity — every feature we talk about is also available to objects created via dependency injection.

Argument Replacement

ENOUGH ALREADY! Let’s get to it. Argument replacement is a powerful feature that gives us full control, via configuration, of what the object Manager will inject in the __construct method.

Like a lot of features in Magento 2, argument replacement is best understood by example. Consider our Example class’s constructor

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php    
public function __construct(
    ExampleArgument1 $object1,
    ExampleArgument2 $object2,
    $scaler1='foo',
    $scaler2=0,
    $scaler3=false,
    $thearray=['foo'])        
{
    $this->object1 = $object1;
    $this->object2 = $object2;    

    $this->scaler1 = $scaler1;
    $this->scaler2 = $scaler2;
    $this->scaler3 = $scaler3;        
    $this->thearray   = $thearray;                
}

Let’s say we didn’t want $scaler1 to be equal to foo. The object manager and dependency injection block us from using normal constructor parameters, so up until now we’ve been stuck. This is what argument replacement lets us do. Add the following nodes to the di.xml file

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

We’ll get to what our new <type/> node does in a second, but first lets clear our cache

$ php bin/magento cache:clean --all
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 then re-run our ps:tutorial-object-manager-arguments command. You’ll recall this command shows us the values of our Example object’s properties.

$ php bin/magento ps:tutorial-object-manager-arguments
#... snipped ...
The Property $scaler1
  is a string
  with a value of: bar
#... snipped ...       

You should see the value of the $scaler property has changed from foo to bar. This is what argument replacement does — it allows end-user-programmers to change the value of any dependency injected argument.

How it Works

Let’s take a look at our di.xml configuration again

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config>       
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <!-- ... --->
    </type>
</config>

Here we’ve introduced a new-to-us second-level configuration node named <type/>. The node refers to the class whose arguments we’re trying to change, in our case that’s Pulsestorm\TutorialObjectManagerArguments\Model\Example. The type name refers not to native PHP types, but instead the idea that all the classes you define in your system/application form their own type system.

For Magento 1 developers, another way to think of types might be class aliases

Mage::getModel('catalog/product');

Back in Magento 1 these class aliases also formed a type system. While Magento 2 eschews class aliases, the object manager effectively turns each class name (or, with our new nomenclature, type name) into an alias.

So, the <type/> node lets us tell Magento 2 which class’s argument we want to target. The inner nodes let us tell Magento 2 what we want to do with the arguments

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<config>       
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

The outer <arguments/> node lets Magento know we’re dealing with arguments — there’s other <type/> sub-nodes we’ll cover in future articles. Each single <argument/> node (no S) lets us change the inject value of a single __construct argument.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    

<argument name="scaler1" xsi:type="string">bar</argument>                   

The name of an argument comes from its PHP variable name as a parameter. i.e., because the parameter is named $scaler1

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php
public function __construct(
    //...
    $scaler1='foo',
    //...
{

the <argument/> attribute name is set to scaler1. The xsi:type node lets us tell Magento what sort of value we want to replace the existing value with. With those attributes set — Magento will use the inner text value of <argument/> as the new value to inject, (in our case, that’s bar).

The first time I saw a non-object parameter in a Magento 2 class’s __construct method I didn’t understand why it was there. It seemed like the object manager and type hint dependency injection both would preclude this parameter from being anything other than its default value. Once I discovered the argument replacement feature though, they made a lot more sense.

Let’s dive a little deeper into argument replacement. There’s additional nuances you’ll need to get the most from this feature.

Replacing Object Arguments

Speaking of objects, what do you think would happen if we tried replacing one of the object parameters? Let’s give it a try! Add the following node to your di.xml configuration.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="object1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

This is very similar to our previous configuration. The only difference is we’ve targeted the parameter named object1. Let’s clear our cache and try running our command with the above in place.

$ php bin/magento ps:tutorial-object-manager-arguments

  [ErrorException]                  
  Illegal string offset 'instance'  

Whoops! An error. While the error is somewhat cryptic, this is correct system behavior. We just tried to replace the $object1 parameter with a string, but remember that all dependency injected arguments have type hints.

namespace Pulsestorm\TutorialObjectManagerArguments\Model;
public function __construct(
    ExampleArgument1 $object1,
    //...
)

It’s impossible to replace an injected object with a non-object. Fortunately, it is possible to replace an injected object with a different object! Give the following configuration a try

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="object1" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent</argument>
        </arguments>
    </type>

</config>

We’ve changed two things in the above configuration. First, the xsi:type argument is now set to object. This lets the object manager know it should treat the node’s text content as a class instead of a raw string. This leads us nicely into the other thing we’ve changed, which is to set the <argument/> node’s contents to Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent. This is the object we want to replace the original with.

Clear your cache, run your command, and you should see the following.

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent

That is, Magento is now injecting a Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent class for the first argument.

Inserting Class Constants

Here’s another feature the xsi:type attribute enables. Give the following a try

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler2" xsi:type="const">Magento\Integration\Model\Integration::SETUP_TYPE</argument>
        </arguments>
    </type>
</config>

Here we’ve used an xsi:type of const, and a node value of Magento\Integration\Model\Integration::SETUP_TYPE to replace the scaler2 argument. Run the reflection command with the above configuration and you’ll see the following

$ php bin/magento ps:tutorial-object-manager-arguments
//... snipped ...
The Property $scaler2
  is a string
  with a value of: setup_type
//... snipped ...

So — where did the setup_type value come from? The const value in xsi:type allows you to insert the value of a class constant. Magento interprets the node value (Magento\Integration\Model\Integration::SETUP_TYPE) as the class Magento\Integration\Model\Integration and the constant SETUP_TYPE

#File: app/code/Magento/Integration/Model/Integration.php
namespace Magento\Integration\Model;
//...
class Integration extends \Magento\Framework\Model\AbstractModel
{

    //...
        const SETUP_TYPE = 'setup_type';
    //...
}

Replacing Arrays

PHP arrays are a bit of a special case for argument replacement. Let’s try replacing our thearray parameter by adding the following configuration to di.xml

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="thearray" xsi:type="array">
                <item name="0" xsi:type="string">science</item>
                <item name="baz" xsi:type="string">baz</item>
                <item name="bar" xsi:type="string">bar</item>
            </argument>
        </arguments>
    </type>
</config>    

You’ll notice that instead of a simple string inside of <argument/>, there’s a new set of <item/> nodes. If we run our command with the above configuration, we should see the following

$ php bin/magento ps:tutorial-object-manager-arguments -v
//...

The Property $thearray
  is an array
  with the elements: 
  0=>science
  baz=>baz
  bar=>bar

That is, we’ve replaced the default array with a multiple item, mixed key array. If we take a closer look at the <item/> tags

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">baz</item>

We see that Magento will interpret an items name (baz) as an array key, and it will interpret the tag contents (baz) as the value. One interesting thing here is the xsi:type tag. This works the same as in argument — which means you can create an array with scalars (above), objects

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">Some\Php\Class</item>

or even other, nested arrays!

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">
    <item name="0" xsi:type="string">one</item>
    <item name="1" xsi:type="string">two</item>        
</item>    

Another interesting feature of arrays is how Magento handles multiple modules trying to “replace” the same array. For objects, and strings, and other scalar xsi:types like number or boolean, Magento operates on a “last module in wins” principle.

With arrays, however, Magento will merge the <items/>. This means it’s possible to have multiple modules contributing items to an array. In fact, for the past few articles, we’ve been relying on this functionality!

If you take a look at this module’s di.xml file, you’ll see the following nodes.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="testbedCommand" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Command\Testbed</item>
        </argument>
    </arguments>
</type>  

This is the same sort of argument replacement we’ve been doing in this article. That is, we’re replacing (or, since it’s an array, merging) the commands parameter in the core Magento\Framework\Console\CommandList class. We’re merging in an Pulsestorm\TutorialObjectManagerArguments\Command\Testbed object.

If we look at the class’s constructor

#File: lib/internal/Magento/Framework/Console/CommandList.php
namespace Magento\Framework\Console;
//...
class CommandList
{
    //...
    public function __construct(array $commands = [])
    {
        $this->commands = $commands;
    }
    //...
}

We see $commands is an array. In fact — this is an array that contains a list of commands for the bin/magento command. All Magento modules add commands this way. You should be familiar with the cache:clean command. This command’s class file is here

#File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php
<?php    //...
namespace Magento\Backend\Console\Command;

class CacheCleanCommand extends AbstractCacheTypeManageCommand
{
    //...
}

However, it’s this module’s di.xml file that makes it available to the command line framework.

<!-- #File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php -->
<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <!-- ... snip ... -->
            <item name="cacheCleanCommand" xsi:type="object">Magento\Backend\Console\Command\CacheCleanCommand</item>
            <!-- ... snip ... -->
        </argument>
    </arguments>
</type>

This is just one example of how the core Magento framework treats dependency injection and the object manager as first class citizens.

Best Practices for Type Safety

While Magento’s use of PHP’s native type hints help enforce type safety during argument replacement, it is (as of this writing) technically possible to replace a scaler argument with an object. The configuration for that would look something like this

<!-- #File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php -->
<type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
    <arguments>
        <argument name="scaler1" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent</argument>
    </arguments>
</type>

Although this configuration will run, it does lead to some odd results. If we run our reflection command with the above configuration in place

$ php bin/magento ps:tutorial-object-manager-arguments -v
The Property $scaler1
  is an array
  with the elements: 
  instance=>Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent

We see that Magento has, for reasons that are unclear and likely an unintended side effect, replaced the argument with an array instead of an object, and that the array contains the name of the object.

While the more clever and resourceful among you might start thinking of ways to take advantage of this behavior, I’d advise against it. Even if it did work, replacing an argument PHP expects to use as a string, number, etc, with an object would likely have unforeseen consequences.

Also, while we’re here, here’s a list of all the valid (as of this writing) xsi:types

xsi:type="array"
xsi:type="string"
xsi:type="object"
xsi:type="boolean"
xsi:type="const"
xsi:type="number"
xsi:type="string"
xsi:type="init_parameter"
xsi:type="null"

Most of these are self explanatory. The only non-scaler type we didn’t discuss was init_parameter which, at this point, is just a de-facto alias for const.

Selective Rewrites

From the point of view of a Magento 1 developer, argument replacement offers a more selective version of the old class rewrite functionality. In our example above — if we needed to change the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1 — (or something like tutorialobjectmanager/exampleargument1 in Magento 1 class alias speak) the only way to do it was with a global rewrite that changed the behavior of the class in the entire system. Not taking adequate care to maintain the old functionality was created extension conflicts and system instability in Magento 1.

While it’s not fool proof, Magento 2’s argument replacement feature allows us to effectively change the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1, but only when this class is used in the Pulsestorm\TutorialObjectManagerArguments\Model\Example class. This means the rest of the system is still using the original class, and there’s zero chance our argument replacement will effect those systems.

Of course, you are still changing the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\Example system-wide, so it’s not fool proof, but speaking for myself it’s a welcome addition to Magento’s functionality.

ALL That said, there are features beyond argument replacement that go even further in stabilizing Magento customization. Out next stop on the Magento object manager train will be the virtualType system.

Originally published July 29, 2015

Magento 2 Object Manager Preferences

Like this article? 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.

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.

  • The main execute method calls the getMessage method of the object stored in the messenger object property.

  • The command code sets this messenger property in the constructor, and Magento creates the object itself via __construct dependency injection.

  • Re: the automatic dependency injection — the type hint of Messenger (full class name of Pulsestorm\TutorialObjectPreference\Model\Messenger) ensures the $messenger parameter contains an instantiated Pulsestorm\TutorialObjectPreference\Model\Messenger object.

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 --all
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