Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Dependency injection

Dependency injection is a way to hand a class the objects it needs to do its job. In Magento objects are not created with new: a class lists what it needs in its constructor, and a special part of the core — the object manager — creates and passes everything itself. You already used this when an action received ResultFactory.

Your own service class

Let's create a class that builds a greeting and writes a line to the log. Classes with logic go into the module's Model folder; the file is created by hand.

// app/code/Acme/Hello/Model/Greeter.php
namespace Acme\Hello\Model;
 
use Psr\Log\LoggerInterface;
 
class Greeter
{
    public function __construct(private LoggerInterface $logger) {}
 
    public function greet(string $name): string
    {
        $this->logger->info('Greeting for ' . $name);
 
        return 'Hello, ' . $name . '!';
    }
}

The class asks for a logger by the LoggerInterface type. Greeter does not have to be registered anywhere: Magento creates it when somebody needs it.

Using it

To get a Greeter, another class asks for it in its constructor too:

// app/code/Acme/Hello/ViewModel/Greeting.php
namespace Acme\Hello\ViewModel;
 
use Acme\Hello\Model\Greeter;
use Magento\Framework\View\Element\Block\ArgumentInterface;
 
class Greeting implements ArgumentInterface
{
    public function __construct(private Greeter $greeter) {}
 
    public function getGreeting(): string
    {
        return $this->greeter->greet('Anna');
    }
}

Print $escaper->escapeHtml($viewModel->getGreeting()) in the template, delete the contents of the generated folder and open /hello. The page shows "Hello, Anna!", and a new line appears in the var/log/system.log file.

How Magento picks an object

The object manager looks at the types of the constructor parameters. If the type is a class, an object of that class is created. If the type is an interface, a rule is needed that says which class stands behind it. Such a rule is called a preference and is written in a di.xml file. For LoggerInterface the core already has one.

Your own interface and a preference

By convention a module's interfaces live in the Api folder:

// app/code/Acme/Hello/Api/GreeterInterface.php
namespace Acme\Hello\Api;
 
interface GreeterInterface
{
    public function greet(string $name): string;
}

Add implements \Acme\Hello\Api\GreeterInterface to the Greeter class and create the etc/di.xml file:

<?xml version="1.0"?>
<!-- app/code/Acme/Hello/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Acme\Hello\Api\GreeterInterface" type="Acme\Hello\Model\Greeter"/>
</config>

Now the Greeting constructor can ask for GreeterInterface — a Greeter object arrives. After editing di.xml run bin/magento cache:clean. The benefit is that another module can substitute its own implementation without changing your code.

app/code/Acme/Hello/
├── Api/
│   └── GreeterInterface.php    new file
├── Model/
│   └── Greeter.php             new file
├── ViewModel/Greeting.php
└── etc/
    ├── di.xml                  new file
    └── module.xml

Where di.xml applies

FileWhere it applies
etc/di.xmlEverywhere
etc/frontend/di.xmlOnly on the storefront
etc/adminhtml/di.xmlOnly in the admin panel

Generated classes

To make injection work, Magento creates helper classes in the generated folder. In developer mode this happens on the fly, so after changing a constructor it is enough to clear the folder. On a production server everything is built in advance with bin/magento setup:di:compile.

Common mistake

Calling ObjectManager::getInstance()->get(...) right in your class works but defeats the whole idea: dependencies become invisible, they cannot be replaced through di.xml or checked by a test. List everything a class needs in its constructor.

Thanks for reading!

Donate to the site — we work hard for you.