Preferences, Types and Virtual Types
Welcome to another part of the Magento 2 Design Patterns miniseries. Today, let’s have a look at the three ways you can expand your code with Preferences, Types and Virtual Types
Preference
Preference is used by the Object Manager to indicate the default implementation. If you try to inject an interface in the constructor but don’t map it with <preference/>
in the di.xml file, you will get a PHP error: `PHP Fatal error: Uncaught Error: Cannot instantiate interface X in …`.
Many classes can implement the interface, but the ObjectManager needs to know its default implementation. Hence, you need to indicate it in the di.xml
file.
<preference for="Vendor\Module\InterfaceX"
type="Vendor\Module\ClassThatImplementsInterfaceX"/>
Preference
doesn’t have to be used with interfaces only. You can use it to rewrite a class from another module to point at the implementation, which will cause the class to be used globally. It’s not recommended unless you know what you’re doing (later we will discuss how to pass the implementation in a specific class only):
<preference for="Vendor\Module\ClassToBeRewrited"
type="Vendor\Module\RewritedClass"/>
When will preference
not work?
You need to remember that preference
only works when injected by the ObjectManager. So, if you write in your code new ClassThatShouldBeRewrited()
, then this particular class will be used instead of the one we’ve rewritten through preference
.
It won’t work when extending either. PHP works here by default, so if a class extends a class that should be rewritten, then the original one will be used instead of the one you rewrote with. The same thing applies to abstract classes. They are only used for extending and you can’t use them to create an object directly.
It’s also worth noting that the rewriting class has to extend the rewritten class or at least implement the same interface. If the code ever requires ClassThatWillBeRewrited
and we use the ClassThatIsRewriting
class which doesn’t extend ClassThatWillBeRewrited
, you will get a fatal error linked to the passed argument type.
Similarly, preference
won’t work with final classes which can’t be extended.
Type
Node <type/>
lets us manipulate the dependencies injected to class constructors. It’s a powerful tool that e.g. allows us to add new Routers in Magento.
I already mentioned type in the previous article, when we passed Proxy to the class constructor.
Here’s an example of how type can be used:
<type name="Vendor\Module\ClassA">
<arguments>
<argument name="someClassInjectedIntoConstructor" xsi:type="object">Vendor\Module\OurOwnClass</argument>
</arguments>
</type>
We basically pass an object of our Vendor\Module\OurOwnClass
class to the ClassA
constructor of the someClassInjectedIntoConstructor
variable
Let’s say that we want to use a different method implementation in a class but we don’t want to do it globally through preference
. Thanks to type
, we can do just that.
But type isn’t just about objects. It can be any value that can be passed to the constructor. Let’s say the constructor accepts a table of values, say fruits:
<?php
namespace Vendor\Module;
class Blender
{
private $fruits;
public function __construct(array $fruits = [])
{
var_dump($fruits);
}
}
You can pass values in your di.xml
file:
<type name="Vendor\Module\Blender">
<arguments>
<argument name="fruits" xsi:type="array">
<item name="apple" xsi:type="string">Apple</item>
<item name="orange" xsi:type="string">Orange</item>
</argument>
</arguments>
</type>
All coming to a predictable result:
array(2) {
["apple"]=>
string(5) "Apple"
["orange"]=>
string(6) "Orange"
}
Now let’s say the code above is in a different module. We don’t have to (and we shouldn’t) modify that module’s code directly. Instead, we can pass more values in our module.
<type name="Vendor\Module\Blender">
<arguments>
<argument name="fruits" xsi:type="array">
<item name="banana" xsi:type="string">Banana</item>
<item name="grape" xsi:type="string">Grape</item>
</argument>
</arguments>
</type>
Output:
array(4) {
["apple"]=>
string(5) "Apple"
["orange"]=>
string(6) "Orange"
["banana"]=>
string(6) "Banana"
["grape"]=>
string(5) "Grape"
}
It’s plain to see that Magento merged the two tables. Routers and Commands work in the same way.
Virtual Types
The last thing I’d like to cover in this article is virtualType. Virtual types allow us to modify existing classes and inject them where we need them.
<virtualType/>
accepts two attributes – name
and type
. Name
is the virtualType’s name and type
is an existing class we use to create the virtualType.
The following chunk of code:
<virtualType name="MyVirtualType" type="Vendor\Module\MainClass">
</virtualType>
Is almost the same as creating a PHP class:
<?php
class MyVirtualType extends \Vendor\Module\MainClass
Creating a PHP class would give us more possibilities, like modifying methods or simply using the class inside PHP code. In the case of a virtualType, we can modify the arguments being passed to the constructor. Also, you can only use virtual types within di.xml
( a class created by virtualType in di.xml
will not work if we try to use it in PHP code).
What can you do with this virtualType, now that you’ve created it? Well, you can inject it into another class using the aforementioned type
Let’s juice things up
Sounds confusing? Let’s try with the fruit example again.
Say you want to make a fruit juice for a party and each guest is told to bring a bowl of fruits.
First, use the Blender class in your code, where you’ll throw the fruit bowls’ contents.
<?php
namespace Vendor\Module;
class Blender
{
public function __construct(array $bowls = [])
{
var_dump($bowls);
}
}
Next, put all fruits into the Bowl class:
<?php
namespace Vendor\Module;
class Bowl
{
private $fruits;
public function __construct(array $fruits = [])
{
$this->fruits = $fruits;
}
}
Continuing the example, combine the fruit bowls and then move them into the blender. Let’s assume you first create 2 bowls; one with apples and bananas and another with oranges and grapefruits.
<virtualType name="FirstBowl" type="Vendor\Module\Bowl">
<arguments>
<argument name="fruits" xsi:type="array">
<item name="apple" xsi:type="string">Apple</item>
<item name="banana" xsi:type="string">Banana</item>
</argument>
</arguments>
</virtualType>
<virtualType name="SecondBowl" type="Vendor\Module\Bowl">
<arguments>
<argument name="fruits" xsi:type="array">
<item name="orange" xsi:type="string">Orange</item>
<item name="grapefruit" xsi:type="string">Grapefruit</item>
</argument>
</arguments>
</virtualType>
Thanks to the virtualTypes we managed to create two virtual classes sharing the same logic (holding fruits) but different contents (fruits types). Without a virtualType, we’d now have to create separate classes that would extend Vendor\Module\Bowl
.
Now that we have our bowls ready, let’s move them inside Blender:
<type name="Vendor\Module\Blender">
<arguments>
<argument name="bowls" xsi:type="array">
<item name="firstBowl" xsi:type="object">FirstBowl</item>
<item name="secondBowl" xsi:type="object">SecondBowl</item>
</argument>
</arguments>
</type>
Voilà – the bowls are now in the blender and you are ready to make some juice 😉
array(2) {
["firstBowl"]=>
object(Vendor\Module\Bowl)#660 (1) {
["fruits":"Vendor\Module\Bowl":private]=>
array(2) {
["apple"]=>
string(5) "Apple"
["banana"]=>
string(6) "Banana"
}
}
["secondBowl"]=>
object(Vendor\Module\Bowl)#661 (1) {
["fruits":"Vendor\Module\Bowl":private]=>
array(2) {
["orange"]=>
string(5) "Orange"
["grapefruit"]=>
string(6) "Grapefruit"
}
}
}
Remember that by executing an operation on the Dependency Injection, if you are not in the developer’s mode and don’t clean generated/code
(or var/generated/
), you have to execute php bin/magento setup:di:compile
to see the results.
That’s all for today, good luck!