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.xmlWhere di.xml applies
| File | Where it applies |
|---|---|
etc/di.xml | Everywhere |
etc/frontend/di.xml | Only on the storefront |
etc/adminhtml/di.xml | Only 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.