Categories


Archives


Recent Posts


Categories


Magento 2 Object Manager Virtual Types

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!

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
Series Navigation<< Magento 2 Object Manager Argument ReplacementMagento 2 Object Manager: Proxy Objects >>