Magento 2 Dependency Injection and Swapping Dependencies with Virtual Types

Recently we came across a scenario where we wanted to swap dependencies depending on the class that required them. The scenario was that we were using either the console or logging as an output when writing a module to use an API for product data among other things.

We wanted to be able to easily swap out the class that handled the output to logging or console (one for each output using an interface contract to standardise the classes)

Suppose we have an output contract aptly named OutputContract

interface OutputContract
{
    /**
     * @param string $message
     */
    public function write($message);

    /**
     * @param string $message
     */
    public function writeln($message);
}

We would then have two classes that implement this contract also aptly named Console and Logger (these can be placed in a namespace to group them together as implementations).

In Magento 2 dependency injection we can explicitly declare what dependencies a specific class will be given but we can also declare a virtual type. In this instance we have two classes that will use the output contract, one that is used when running under cron where we want to output to a log file and the other that will use used when running a command using bin/magento

Here is a sample di.xml that is used by a module to specify the dependencies.

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="EdmondsCommerce\Module\Contracts\OutputContract" type="EdmondsCommerce\Module\Helper\Output\Logger"/>
</config>

Here we have specified that we want the output contract to use the logger output by default which is a sane default as everything is written to a log file to be referred to if issues arise. When the interface is type hinted in a constructor we will get the type specified in the preference for that interface.

As for swapping out the dependency we can do the following.

    ...
    <virtualType name="SimpleConsoleImport" type="EdmondsCommerce\Module\Helper\Importer">
        <arguments>
            <argument name="output" xsi:type="object">EdmondsCommerce\Module\Helper\Output\Console</argument>
        </arguments>
    </virtualType>
    
    <type name="EdmondsCommerce\Module\Console\Api\Products">
            <arguments>
                <argument name="productImport" xsi:type="object">SimpleConsoleImport</argument>
            </arguments>
        </type>
    ...

Here instead of using a direct implementation or interface we have specified a virtual type. The virtual type declares that there is a pseudo class named SimpleConsoleImport that uses a different implementation of the OutputContract which writes to the console instead of to a log file. This class is then used by the console command and so the output is written where we want it.

This is a simple demonstration of how we can swap dependencies using the virtual type system which gives a lot of power to the developer which slightly resembles the rewrite system with swapping out classes for bespoke code.


Tags: magentomagento2dependency injectiondesign pattern