Dependency injection is the beating heart of Magento 2’s architecture. It is also the concept that confuses newcomers most, because the object manager hides so much of the wiring. Once DI clicks, the rest of the framework - plugins, virtual types, factories - falls into place. Here is the mental model that works.
Constructor Injection: The Core Idea
A class declares what it needs in its constructor; Magento’s object manager supplies it:
namespace Acme\StoreInfo\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Psr\Log\LoggerInterface;
class StockReporter
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private LoggerInterface $logger
) {}
public function forSku(string $sku): int
{
$product = $this->productRepository->get($sku);
// ...
}
}
You never write new ProductRepository(). The object manager reads the constructor via reflection, resolves each dependency (recursively), and injects instances. This gives you testability (inject mocks in unit tests) and swappability (change one implementation in config, affect the whole application).
di.xml: The Wiring Diagram
etc/di.xml configures how the object manager resolves things. Its main tools:
Preferences - map an interface to a concrete class:
<preference for="Magento\Catalog\Api\ProductRepositoryInterface"
type="Magento\Catalog\Model\ProductRepository"/>
This is why you should always type-hint interfaces: any module can substitute an implementation globally via preference.
Arguments - configure constructor parameters:
<type name="Acme\StoreInfo\Model\StockReporter">
<argument name="warehouseCode" xsi:type="string">UK-MAIN</argument>
</type>
Virtual types - a named configuration of an existing class, without writing new PHP:
<virtualType name="Acme\StoreInfo\Model\CachedStockReporter"
type="Acme\StoreInfo\Model\StockReporter">
<arguments>
<argument name="cacheLifetime" xsi:type="number">3600</argument>
</arguments>
</virtualType>
Magento’s own catalog uses virtual types extensively - many “classes” in core di.xml have no PHP file at all.
Factories and Proxies
Two generated-code patterns you will meet constantly:
- Factory (
ProductFactory): generatesnewinstances on demand. Injected when you need to create objects rather than receive one shared instance - entities like products and orders are classic cases. - Proxy (
ProductRepositoryInterface\Proxy): a lazy-loading stand-in. The real object is only instantiated when first used. Inject a proxy for heavy dependencies that are only needed on some code paths - Magento’s own configuration injects proxies for exactly this reason.
Both are generated automatically; you reference ClassName\Factory or ClassName\Proxy in your constructor and Magento writes the code.
The Golden Rules
- Never call the object manager directly (
ObjectManager::getInstance()->get(...)) outside factories and a few sanctioned cases - it hides dependencies and breaks testing - Type-hint interfaces where the core provides them
- Shareable services (the default) for stateless classes;
shared="false"only when you truly need a fresh instance per injection - Run
bin/magento setup:di:compilein CI - it catches DI misconfigurations before production does
DI looks like ceremony until the first time you swap an implementation across an entire store with three lines of XML. Then it looks like the bargain it is.