Debugging Dependency Injection
Use dev:di:info to find the class Magento really instantiated, and recognise when stale generated code needs a recompile.
bin/magento dev:di:info <class> [<area>]Why your edit changed nothing
Two mechanisms stand between the file you edited and the object that ran. A preference in some module's di.xml may say that requests for your interface, or even for your class, are answered with a different class. And every class that has a plugin is replaced at runtime by a generated subclass named <Class>\Interceptor. Editing the original is correct; assuming the original is what executed is not.
dev:di:info
bin/magento dev:di:info <class> [<area>] prints the merged picture for one class: the preference in force, each constructor parameter with its requested type and any configured value, the virtual types based on it, and the plugins attached to it. The area argument is optional and defaults to global. Supply it, because etc/frontend/di.xml, etc/adminhtml/di.xml and etc/webapi_rest/di.xml merge on top of etc/di.xml and can disagree.
Generated code
generated/code holds interceptors, factories and proxies as real PHP files. generated/metadata holds the compiled DI configuration. In developer mode both are written on demand, so deleting generated/code is usually enough to clear a stale class. In production mode nothing is generated at runtime: after changing a constructor or any di.xml you must run bin/magento setup:di:compile and then bin/magento cache:flush.
What trips people up
Deleting generated/ on a production-mode site takes the store down until the compile finishes. Do it inside maintenance mode.
Example
<!-- app/code/Vendor/Pricing/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Asking for the interface gets HttpFeedReader. -->
<preference for="Vendor\Pricing\Api\FeedReaderInterface"
type="Vendor\Pricing\Model\HttpFeedReader"/>
<!-- A named, pre-configured HttpFeedReader. No PHP file exists for it. -->
<virtualType name="Vendor\Pricing\Model\NightlyFeedReader"
type="Vendor\Pricing\Model\HttpFeedReader">
<arguments>
<argument name="timeout" xsi:type="number">120</argument>
</arguments>
</virtualType>
<!-- CatalogSync asks for FeedReaderInterface but receives the virtual type. -->
<type name="Vendor\Pricing\Model\CatalogSync">
<arguments>
<argument name="reader" xsi:type="object">Vendor\Pricing\Model\NightlyFeedReader</argument>
</arguments>
</type>
</config>When to use it
- A plugin you registered never fires and you need to know whether Magento sees it.
- A constructor argument has a value you cannot find in the class or in your own di.xml.
- A di.xml edit works locally and does nothing on the staging server.
- A fatal error names a class under generated/code that no longer matches your constructor.
More examples
Ask Magento what it will build
This is the first command to run when a class behaves as though your configuration is not there. The preference line answers which class was built. The plugin table lists the plugin class, the method it intercepts and its type, before, around or after; it does not print sort order, so it answers who else is involved rather than in what order, and it tells you whether a plugin you wrote was registered at all. Quote the class name in a shell, or the backslashes will be eaten before Magento sees them. The command only reads configuration, so it is safe on a live store.
# Global area, by default.
bin/magento dev:di:info 'Vendor\Pricing\Model\CatalogSync'
# The same class as the storefront sees it.
bin/magento dev:di:info 'Magento\Catalog\Api\ProductRepositoryInterface' frontend
# Output is a set of tables. Abbreviated:
#
# DI configuration for the class Magento\Catalog\Api\ProductRepositoryInterface
# in the FRONTEND area
#
# Preference: Magento\Catalog\Model\ProductRepository
#
# Plugins:
# +---------------------------------------+--------+-------+
# | Plugin | Method | Type |
# +---------------------------------------+--------+-------+
# | Vendor\Pricing\Plugin\LogProductLoad | get | after |
# +---------------------------------------+--------+-------+
#
# Plugins for the Preference:
# ... the same table, for Magento\Catalog\Model\ProductRepository ...Clear stale generated code, per mode
The two modes need opposite habits. In developer mode deleting generated/code is cheap and fixes almost every stale interceptor or factory. In production mode the same deletion is an outage: the autoloader finds no interceptor classes and every request fails until setup:di:compile has run, which takes minutes on a large install. Maintenance mode around the production sequence is what keeps customers off the site while that happens.
bin/magento deploy:mode:show
# Current application mode: developer.
# Developer mode: the files are rebuilt on the next request.
rm -rf generated/code/*
bin/magento cache:flush
# Production mode: nothing is generated at runtime, so order matters.
bin/magento maintenance:enable
bin/magento setup:upgrade --keep-generated
bin/magento setup:di:compile
bin/magento cache:flush
bin/magento maintenance:disableConfirm you are inside an interceptor
Worth running once, so the naming stops being surprising. The object is an Interceptor subclass generated because something has a plugin on that class; its parent is the real implementation chosen by the preference. This is why a var_dump of a Magento object shows a class name you never wrote, why a breakpoint on the original method is reached through ___callParent, and why instanceof against a concrete class still works while a strict class name comparison does not. Use the interface in type declarations and the problem does not arise.
<?php
// app/code/Vendor/Pricing/Console/Command/WhichClassCommand.php
// (abbreviated: only execute() is shown)
declare(strict_types=1);
use Magento\Catalog\Api\ProductRepositoryInterface;
// $this->productRepository was injected as ProductRepositoryInterface.
$repository = $this->productRepository;
$output->writeln(get_class($repository));
$output->writeln(get_parent_class($repository) ?: '(no parent)');
// Terminal output:
// Magento\Catalog\Model\ProductRepository\Interceptor
// Magento\Catalog\Model\ProductRepository
Discussion