Magento already provides the useful command dev:di:info for inspecting dependency injection. However, it does not display an entire plugin chain for a particular method. A small custom CLI command can retrieve that information directly from Magento’s interception configuration.
The following command is therefore not a standard Magento command, but is implemented in the example below:
|
1 2 3 |
bin/magento dev:plugin-chain \ 'Magento\Catalog\Api\ProductRepositoryInterface' \ save |
First, register the command in etc/di.xml registered by your own module:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> <item name="plugin_chain" xsi:type="object"> Vendor\Module\Console\Command\PluginChain </item> </argument> </arguments> </type> </config> |
The command class then accesses Magento’s PluginListInterface zu:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
<?php declare(strict_types=1); namespace Vendor\Module\Console\Command; use Magento\Framework\Config\ScopeInterface; use Magento\Framework\Interception\DefinitionInterface; use Magento\Framework\Interception\PluginListInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class PluginChain extends Command { private const ARG_TYPE = 'type'; private const ARG_METHOD = 'method'; private const OPT_AREA = 'area'; public function __construct( private readonly PluginListInterface $pluginList, private readonly ScopeInterface $configScope ) { parent::__construct(); } protected function configure(): void { $this->setName('dev:plugin-chain') ->setDescription('Shows the effective Magento plugin chain') ->addArgument( self::ARG_TYPE, InputArgument::REQUIRED, 'Class or interface' ) ->addArgument( self::ARG_METHOD, InputArgument::REQUIRED, 'Method name' ) ->addOption( self::OPT_AREA, null, InputOption::VALUE_REQUIRED, 'Magento area', 'frontend' ); parent::configure(); } protected function execute( InputInterface $input, OutputInterface $output ): int { $type = ltrim( (string) $input->getArgument(self::ARG_TYPE), '\\' ); $method = (string) $input->getArgument(self::ARG_METHOD); $area = (string) $input->getOption(self::OPT_AREA); $this->configScope->setCurrentScope($area); $pluginInfo = $this->pluginList->getNext( $type, $method ); if (!$pluginInfo) { $output->writeln('<comment>No plugins found.</comment>'); return Command::SUCCESS; } $output->writeln( sprintf( '<info>%s::%s() [%s]</info>', $type, $method, $area ) ); $this->renderChain( $output, $type, $method, $pluginInfo ); return Command::SUCCESS; } private function renderChain( OutputInterface $output, string $type, string $method, array $pluginInfo, int $level = 0 ): void { $indent = str_repeat(' ', $level); foreach ( $pluginInfo[DefinitionInterface::LISTENER_BEFORE] ?? [] as $code ) { $this->writePlugin( $output, $type, $code, 'BEFORE', $indent ); } $around = $pluginInfo[ DefinitionInterface::LISTENER_AROUND ] ?? null; if ($around) { $this->writePlugin( $output, $type, $around, 'AROUND', $indent ); $next = $this->pluginList->getNext( $type, $method, $around ); if ($next) { $this->renderChain( $output, $type, $method, $next, $level + 1 ); } } foreach ( $pluginInfo[DefinitionInterface::LISTENER_AFTER] ?? [] as $code ) { $this->writePlugin( $output, $type, $code, 'AFTER', $indent ); } } private function writePlugin( OutputInterface $output, string $type, string $code, string $position, string $indent ): void { $plugin = $this->pluginList->getPlugin( $type, $code ); $output->writeln( sprintf( '%s%-6s %s => %s', $indent, $position, $code, get_class($plugin) ) ); } } |
After installing the command, you can inspect the plugin chain for ProductRepositoryInterface::save() ausgeben:
|
1 2 3 |
bin/magento dev:plugin-chain \ 'Magento\Catalog\Api\ProductRepositoryInterface' \ save |
You can also inspect the Magento admin area:
|
1 2 3 4 |
bin/magento dev:plugin-chain \ 'Magento\Catalog\Api\ProductRepositoryInterface' \ save \ --area=adminhtml |
The output becomes particularly interesting when several extensions modify the same method. In particular, around Plugins can control execution: by calling or not calling the next callable, a plugin determines whether the rest of the chain and the original method are reached.
This makes it possible to understand which plugins Magento actually invokes, and in which order, for a given method..