- Magento 2 Object Manager
- Magento 2’s Automatic Dependency Injection
- Magento 2 Object Manager Preferences
- Magento 2 Object Manager Argument Replacement
- Magento 2 Object Manager Virtual Types
- Magento 2 Object Manager: Proxy Objects
- Magento 2 Object Manager: Instance Objects
- Magento 2 Object Manager Plugin System
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
- Instantiates a
Pulsestorm\TutorialVirtualType\Model\Example
object via the object manager, and then reports on it -
Fetches the
Example
object’sproperty_of_example_object
property and reports on the object inside of it (aPulsestorm\TutorialVirtualType\Model\Argument1
object) -
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 affect 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.