Why & How to Implement Virtual Types in Magento 2?

Likes (1)   Dislikes (0)  
  (1 reviews)

Virtual Types in Magento 2 is something that is not an actual class implemented anywhere in our module file system. Rather it is a configured class that is generated automatically for us by Magento 2 code generation library. Yes, we have to just configure a Virtual Type definition in our di.xml file and run the Magento 2 Dependency Injection Compilation process, the di:compile.

The Problem In Hand

There exists one class (B) in a module and we want to use it. Very simple we can add that class to our dependency list in the constructor of our class (A) where we need to use the target class (B). But wait the target class (B) that we are going to use in our module has a dependency on class (C). Yes, you got it right (C) is a class not an interface. The target class (B) uses class (C) and gives output of certain method based on the implementation of class (C).

During interface realisation in Magento 2 we use preference XML configuration in di xml file. Here in this problem statement we have a class (C) as a dependency in the class (B). This kind of dependency realisation is done using type – arguments combination of XML configuration in di xml file.

Magento 2 Dependency Injection

The requirement here is we do not want to repeat the whole cycle to redefine a class that already exists but instead use the existing class. The problem is if we use the target class (B) as is we are not going to get the response that we needed. Instead we will get the default response that is implemented in the target class (B) with respect to its own dependency class (C). Here class (B) acts as an interface or wrapper that actually gives output of class (C).

Let’s Understand The Requirement By An Example

Magento 2 has the capability of templated emailing. We create an email template and use it to send dynamic email of same context. While using this email template we provide the dynamic data to the email template. Composing this piece of code is a little bit long. And it becomes a repeated task if we have to send several different types of templated emails according to our business logic.

Solution #1

Let’s say that to get rid of repeating ourselves we authored a context specific mailer class. And we started using it as an one line code where ever we need to send this email. We hardcoded the email template code in the class and started using it for one purpose.

PHP
class MailerClassForContextA extends AbstractHelper
{
  public function __construct(
    ...
  ) {
    ...
  }
  
  private function composeAndSend(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    // hard coded email template code used here
    ...
  }
  
  public function send(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    $this->composeAndSend($templateVariables, $senderInfo, $recieverInfo);
    ...
  }
}

This way also we have to duplicate this class in different contexts to author different mailer classes with different hardcoded email template code in it.

PHP
class MailerClassForContextB extends AbstractHelper
{
  ...
  ...
}

class MailerClassForContextC extends AbstractHelper
{
  ...
  ...
}

// etc

Solution #2

One solution to avoid duplicating ourselves is to use overriding a base class. By doing so we just alter the code that configures the email template code in the child class. Again, this approach will lead to lots of almost empty classes in our filesystem.

PHP
class BaseMailerClass extends AbstractHelper
{
  protected string $templateCode;
  
  public function __construct(
    ...
  ) {
    ...
  }
  
  protected function composeAndSend(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    // dynamic email template code used here configured in the child class
    ...
  }
  
  public function send(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    $this->composeAndSend($templateVariables, $senderInfo, $recieverInfo);
    ...
  }
}


// child class A
class ChildMailerClassA extends BaseMailerClass
{
  protected string $templateCode = 'email_template_code_for_context_a';
}

// child class B
class ChildMailerClassB extends BaseMailerClass
{
  protected string $templateCode = 'email_template_code_for_context_b';
}

//etc

In the base class we did not declare the template code as a constructor parameter. Because, we will inject this class to the target class constructor where we need to send this email. This way we will not instantiate the class use the new operator but we will let the Magento 2 DI engine to handle this for us.

Solution #3

Let’s take in the problems and the strict requirement from the above 2 solutions.

  • Problem #1: We are always repeating ourselves and that is to create a duplicate block of code.
  • Problem #2: Lot’s of duplicate code in our filesystem.
  • Problem #3: Lot’s of almost empty classes.
  • Requirement #1: We need a single base class that will be able to accept the email template code as an input.
  • Requirement #2: We do not want to create duplicate sub classes of this base class.
  • Requirement #3: But still we want to have context specific class names and variables in our code.
PHP
interface MailerInterface
{

}

class BaseMailerClass extends AbstractHelper implements MailerInterface
{
  private string $templateCode;
  
  public function __construct(
    ...
    ...
    string $templateCode
  ) {
    ...
    ...
    $this->templateCode = $templateCode;
  }
  
  protected function composeAndSend(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    // externally provided email template code used here
    ...
  }
  
  public function send(array $templateVariables, array $senderInfo, array $recieverInfo): void
  {
    ...
    $this->composeAndSend($templateVariables, $senderInfo, $recieverInfo);
    ...
  }
}

XML
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Training\Core\Helper\Email\ChildMailerClassA" type="Training\Core\Helper\Email\Frontend\Template\Base">
        <arguments>
            <argument name="templateCode" xsi:type="string">email_template_code_for_context_a</argument>
        </arguments>
    </virtualType>
    <type name="Training\ModuleA\Controller\Index\ContextA">
        <arguments>
            <argument name="mailerContextA" xsi:type="object">Training\Core\Helper\Email\ChildMailerClassA</argument>
        </arguments>
    </type>

    <virtualType name="Training\Core\Helper\Email\ChildMailerClassB" type="Training\Core\Helper\Email\Frontend\Template\Base">
        <arguments>
            <argument name="templateCode" xsi:type="string">email_template_code_for_context_b</argument>
        </arguments>
    </virtualType>
    <type name="Training\ModuleB\Controller\Index\ContextB">
        <arguments>
            <argument name="mailerContextB" xsi:type="object">Training\Core\Helper\Email\ChildMailerClassB</argument>
        </arguments>
    </type>
</config>

PHP
namespace Training\ModuleA\Controller\Index;

class ContextA extends Action implements HttpPostActionInterface
{
  private MailerInterface $mailerClassA
  
  public function __construct(
    ...
    MailerInterface $mailerContextA
  ) {
    $this->mailerClassA = $mailerContextA;
  }
  
  public function execute()
  {
    ...
    $this->mailerClassA->send($templateVariables, $senderInfo, $recieverInfo);
  }
}

There are various different possibilities to implement virtual types in Magento 2. We just need to understand problem that virtual types solve and how it solves.

The Wrong Solution That We Usually Approach

The silly answer is we will override both the classes (B) & (C) and use dependency injection in di.xml preference to provide our implemented classes whenever (B) & (C) will be requested to the Object Manager. Or we will override only class (C) to tweak the output of class (B) to get different result.

The consequences of the above solution is it will affect entire application. That extra logic processing and that extra output is not required by everyone in the entire application. May be there exist other classes like wise you are trying to use class (B). That class definitely don’t need the extra processing and the additional response.

Best Solution is to use Magento 2 Virtual Type.

We will virtualise the target class (B) and provide our custom implementation of class (delta-c) as argument to the virtualized class (B) using di.xml virtual type declaration.

Magento 2 Virtual Type Implementation Class Diagram
XML
<type name="A">
  <arguments>
    <!-- using the virtual type -->
    <argument name="B" xsi:type="object">VirtualizedB</argument>
  </arguments>
</type>

<!-- defining the virtual type -->
<virtualType name="VirtualizedB" type="B">
  <arguments>
    <argument name="C" xsi:type="object">fully_qualified_class_name_of_extended_type_C</argument>
  </argument>
</virtualType>

Examples of Magento 2 Virtual Types in Core Magento

If you look into Magento 2 core modules, you will find a lot use cases of virtual types out of the box. We will consider the vendor/magento/module_sales here in this example. Upon investigating the etc/di.xml file of the module you will see that there are lots of virtual type examples available. Let’s list a few of them and try to understand their usage.

Virtual type declaration for

  • Magento\Sales\Model\ResourceModel\Metadata
  • Magento\Framework\DB\Sql\ConcatExpression
  • Magento\Sales\Model\EmailSenderHandler
  • Magento\Sales\Observer\Virtual\SendEmails
  • Magento\Sales\Cron\SendEmails
  • Magento\Sales\Model\ResourceModel\Grid

Reading Meta Information For Different Entities in Sales Module

The declaration for type/class Magento\Sales\Model\ResourceModel\Metadata is the most simplest in this module. If we take a look at the constructor of this class it accepts 2 string type arguments, one is the “resource Class Name” and the other one is “model Class Name”.

Constructor of class Magento\Sales\Model\ResourceModel\Metadata
/**
 * @param \Magento\Framework\ObjectManagerInterface $objectManager
 * @param string $resourceClassName
 * @param string $modelClassName
 */
public function __construct(
    \Magento\Framework\ObjectManagerInterface $objectManager,
    $resourceClassName,
    $modelClassName
) {
    $this->objectManager = $objectManager;
    $this->resourceClassName = $resourceClassName;
    $this->modelClassName = $modelClassName;
}

Magento has declared 8 different virtual types for this single class in this module. In each of the virtual type declaration Magento tries to provide the actual fully qualified class name to last 2 arguments. The purpose of this class is to just provide the object instance of the class name given either a new instance or an existing instance.

XML
<virtualType name="orderMetadata" type="Magento\Sales\Model\ResourceModel\Metadata">
    <arguments>
        <argument name="resourceClassName" xsi:type="string">Magento\Sales\Model\ResourceModel\Order</argument>
        <argument name="modelClassName" xsi:type="string">Magento\Sales\Model\Order</argument>
    </arguments>
</virtualType>
<virtualType name="orderItemMetadata" type="Magento\Sales\Model\ResourceModel\Metadata">
    <arguments>
        <argument name="resourceClassName" xsi:type="string">Magento\Sales\Model\ResourceModel\Order\Item</argument>
        <argument name="modelClassName" xsi:type="string">Magento\Sales\Model\Order\Item</argument>
    </arguments>
</virtualType>
...
...
...

Usage of These Virtual Types

The “orderMetadata” is used inside the Order Repository class. If you look into the Order Repository class you will see the first parameter to the class constructor is Magento\Sales\Model\ResourceModel\Metadata.

XML
<type name="Magento\Sales\Model\OrderRepository">
    <arguments>
        <argument name="metadata" xsi:type="object">orderMetadata</argument>
    </arguments>
</type>

The “orderItemMetadata” is used inside the Order Item Repository.

XML
<type name="Magento\Sales\Model\Order\ItemRepository">
    <arguments>
        <argument name="metadata" xsi:type="object">orderItemMetadata</argument>
    </arguments>
</type>

Generate SQL Concat Expression with Specified Columns and Separator

Another class Magento\Framework\DB\Sql\ConcatExpression is also an example of simple virtual type implementation. The purpose of this class is to provide a concatenated sql expression of the specified column names with a specific separator. Here the arguments the column names and the separator are the good candidates for implementation of virtual types.

Class Constructor for Magento\Framework\DB\Sql\ConcatExpression @see vendor/magento/framework/DB/Sql/ConcatExpression.php
/**
 * @param ResourceConnection $resource
 * @param array $columns
 * @param string $separator
 */
public function __construct(
    ResourceConnection $resource,
    array $columns,
    $separator = ' '
) {
    $this->adapter = $resource->getConnection();
    $this->columns = $columns;
    $this->separator = $separator;
}

Magento provides several virtual types for this class as mentioned below. I am listing 2 of them, you can investigate others from your end to learn about them. These virtual types are passed as arguments to another virtual type class in the same file that is for order grid.

XML
<virtualType name="CustomerNameAggregator" type="Magento\Framework\DB\Sql\ConcatExpression">
    <arguments>
        <argument name="columns" xsi:type="array">
            <item name="customer_firstname" xsi:type="array">
                <item name="tableAlias" xsi:type="string">sales_order</item>
                <item name="columnName" xsi:type="string">customer_firstname</item>
            </item>
            <item name="customer_lastname" xsi:type="array">
                <item name="tableAlias" xsi:type="string">sales_order</item>
                <item name="columnName" xsi:type="string">customer_lastname</item>
            </item>
        </argument>
    </arguments>
</virtualType>
<virtualType name="ShippingNameAggregator" type="Magento\Framework\DB\Sql\ConcatExpression">
    <arguments>
        <argument name="columns" xsi:type="array">
            <item name="firstname" xsi:type="array">
                <item name="tableAlias" xsi:type="string">sales_shipping_address</item>
                <item name="columnName" xsi:type="string">firstname</item>
            </item>
            <item name="lastname" xsi:type="array">
                <item name="tableAlias" xsi:type="string">sales_shipping_address</item>
                <item name="columnName" xsi:type="string">lastname</item>
            </item>
        </argument>
    </arguments>
</virtualType>
...
...

Sending Order Emails at Various Stages of an Order Lifecycle

One more class Magento\Sales\Model\EmailSenderHandler is a candidate for virtual type integration. This is one of the complex example of virtual types in the list. This example is somewhat similar with the class diagram although this is entirely in the same module. This single class has the capacity of sending all different types of emails due to virtualization. This class can send following types of emails.

  1. Order Confirmation Email
  2. Order Invoice Email
  3. Order Shipment Email
  4. Order Credit Memo Email
XML
<virtualType name="SalesOrderSendEmails" type="Magento\Sales\Model\EmailSenderHandler">
    <arguments>
        <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\OrderSender</argument>
        <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order</argument>
        <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Collection</argument>
        <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\OrderIdentity</argument>
    </arguments>
</virtualType>
<virtualType name="SalesOrderInvoiceSendEmails" type="Magento\Sales\Model\EmailSenderHandler">
    <arguments>
        <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\InvoiceSender</argument>
        <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Invoice</argument>
        <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Invoice\Collection</argument>
        <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\InvoiceIdentity</argument>
    </arguments>
</virtualType>
...
...

The class can take different types of email sender, resource model, collection and identity container etc as arguments in di.xml to generate different virtual classes. Using the arguments provided to the class via di.xml it generates all different types of emails.

When sending an invoice mail the class has to deal with invoice entity and hence it differs from the shipment email which require a shipment entity to compose the email. Likewise each of the virtual type classes those are generated via compilation differs from each other to fulfill the required purpose.

Conclusion

Magento 2 Virtual Type is a kind of design pattern that solves a kind of problem easily. Read the official Magento 2 documentation here.

Leave a Comment

Share via
Copy link
Powered by Social Snap