Day 4: 4 Ways to Dependency Injection

Today is Day 4 of the Magento Advent Calendar, and I’ll show you three super-practical Dependency Injection examples you can start using immediately.
No theory overload — just real Magento code.

Example 1: Injecting a Service into a Block

We’ll inject a Logger into a block.
This is the simplest and most common DI pattern.

<?php

declare(strict_types=1);

namespace MageMastery\Module\Block;

use Magento\Framework\View\Element\Template;
use Psr\Log\LoggerInterface;

class Info extends Template
{
    public function __construct(
        Template\Context $context,
        private readonly LoggerInterface $logger,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getMessage(): string
    {
        $this->logger->info('Get message from MageMastery');
        return "Hello from MageMastery Advent Day 4!";
    }

    public function getCurrentTimestamp(): string
    {
        return date('Y-m-d H:i:s');
    }
}

Magento’s Object Manager creates this block and gives it the logger automatically.
No manual instantiation. No new Logger().

Example 2: Injecting a Custom Model into a Controller

Here we inject our own service — a custom model — into a controller.
This is the clean way to work with business logic.

<?php

declare(strict_types=1);

namespace MageMastery\Module\Controller\Example;

use MageMastery\Module\Model\DataProvider;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;

class Show implements HttpGetActionInterface
{
    public function __construct(
        private readonly JsonFactory $jsonFactory,
        private readonly DataProvider $dataProvider,
    ) {

    }

    public function execute(): ResultInterface
    {
        $result = $this->jsonFactory->create();
        $result->setData([
            'value' => $this->dataProvider->getValue(),
        ]);
        return $result;
    }
}

And the DataProvider class:

<?php

declare(strict_types=1);

namespace MageMastery\Module\Model;

class DataProvider
{
    public function getValue(): string
    {
        return 'Hello from DataProvider';
    }
}

Example 3: Preference Replacement (Swapping Implementations)

And here’s a more advanced example.
We replace a core interface implementation with a custom class using DI preferences.

<?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="Magento\Catalog\Api\ProductRepositoryInterface"
                type="MageMastery\Module\Model\ProductRepository" />

</config>

Custom ProductRepository class:

<?php

declare(strict_types=1);

namespace MageMastery\Module\Model;

use Magento\Catalog\Api\ProductRepositoryInterface;

class ProductRepository implements ProductRepositoryInterface
{
    public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveOptions = false)
    {
        // TODO: Implement save() method.
    }

    public function get($sku, $editMode = false, $storeId = null, $forceReload = false)
    {
        // TODO: Implement get() method.
    }

    public function getById($productId, $editMode = false, $storeId = null, $forceReload = false)
    {
        // TODO: Implement getById() method.
    }

    public function delete(\Magento\Catalog\Api\Data\ProductInterface $product)
    {
        // TODO: Implement delete() method.
    }

    public function deleteById($sku)
    {
        // TODO: Implement deleteById() method.
    }

    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
    {
        // TODO: Implement getList() method.
    }
}

Now every time Magento needs ProductRepositoryInterface, it uses our custom class instead.
This is one of the most powerful features of DI in Magento.

Example 4: virtualType Instead of Full Preference (Real-World DI Example)

In many Magento projects, you don’t actually want to override an entire interface with a new class via preference.
That’s too heavy-handed and affects the whole system.
Instead, you want a custom version of a repository or model only for one specific class without impacting other functionality.

And that’s exactly what virtualType is made for.

We have a service class that depends on Magento\Catalog\Api\ProductRepositoryInterface.
But for this service only, we want a modified repository that logs extra debug information or loads products differently.

Instead of replacing the interface globally, we use a virtualType just for this one service.

Step 1: Create a virtualType of the existing repository

<virtualType name="MageMastery\Module\Model\ProductRepository"
             type="Magento\Catalog\Model\ProductRepository">
    <arguments>
        <argument name="debugMode" xsi:type="boolean">true</argument>
    </arguments>
</virtualType>

We created a customized “copy” of Magento’s internal ProductRepository class and added a DI argument (debugMode) — without touching the original class and without creating a preference. Magento will construct this repository only when specifically requested.

Step 2: Inject the virtualType into your service

Service using the modified repository:

<?php

declare(strict_types=1);

namespace MageMastery\Module\Model;

class ProductProcessor
{
    private $repository;

    public function __construct(
        \Magento\Catalog\Api\ProductRepositoryInterface $repository
    ) {
        $this->repository = $repository;
    }

    public function process(int $productId)
    {
        $product = $this->repository->getById($productId);
        // processing logic...
        return $product->getName();
    }
}

Step 3: Bind this service to the virtualType (NOT globally)

We don’t want ProductRepositoryInterface everywhere to become our custom version only inside ProductProcessor.

So we override just one dependency:

<type name="MageMastery\Module\Model\ProductProcessor">
    <arguments>
        <argument name="repository" xsi:type="object">
            MageMastery\Module\Model\DebugProductRepository
        </argument>
    </arguments>
</type>

Now the magic happens:

  • Everywhere else in Magento, the repository works normally.
  • But in ProductProcessor, Magento will inject our virtualType, which is a customized clone of ProductRepository.
  • No preferences, no rewriting interfaces, no side effects.

This is a clean and safe way to customize behavior for a specific class without disturbing the rest of the system.

Virtual types allow you to create customized instances of existing classes for specific scenarios. Instead of overriding the entire interface globally with a preference — which can be risky — you create a lightweight virtual type and inject it only where needed.
This is one of the most powerful and safest Dependency Injection techniques in Magento 2.

These are four practical ways to use Dependency Injection in Magento 2:
Injecting services, injecting custom models, and replacing implementations.
Tomorrow is Day 5 — subscribe so you don’t miss it.

You may also want to watch a video I prepared as part of Day 4 of the Magento Advent Calendar dedicated to Dependency Injection.

Similar Posts

Leave a Reply