Plugins — also called interceptors — let you change the behaviour of a public method in any class without editing or replacing that class. They are the main reason Magento modules can coexist: ten modules can each add a plugin to the same method, whereas only one can replace the class with a preference.
How Plugins Work
When a class has plugins, Magento generates an Interceptor subclass in generated/code. The interceptor overrides each plugged method and calls your plugin methods around the original:
- before methods run first and can change the arguments.
- around methods wrap the call and decide whether, and how, the original runs.
- after methods run last and can change the return value.
You register plugins in di.xml — globally in etc/di.xml, or for one area in etc/frontend/di.xml, etc/adminhtml/di.xml, etc/webapi_rest/di.xml or etc/graphql/di.xml.
Example 1: An after Plugin
An after plugin receives the subject and the original result, and must return a result of the same type. Here we log every product saved through the product repository — the kind of small, safe addition after plugins are ideal for. We plug into an interface method, which is preferred because it follows Magento's service contracts.
<?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\Customer\Api\AccountManagementInterface">
<plugin name="mageservices_normalise_customer_names"
type="MageServices\Demo\Plugin\NormaliseCustomerNames"
sortOrder="10"/>
</type>
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<plugin name="mageservices_product_repository_logger"
type="MageServices\Demo\Plugin\LogProductSave"
sortOrder="20"/>
</type>
</config>
<?php
declare(strict_types=1);
namespace MageServices\Demo\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Psr\Log\LoggerInterface;
class LogProductSave
{
public function __construct(
private readonly LoggerInterface $logger
) {
}
public function afterSave(
ProductRepositoryInterface $subject,
ProductInterface $result,
ProductInterface $product,
$saveOptions = false
): ProductInterface {
$this->logger->info('Product saved', ['sku' => $result->getSku()]);
return $result;
}
}
Since Magento 2.2, after plugins also receive the original method arguments after $result, so you can use them without an around plugin.
Example 2: A before Plugin
A before plugin returns an array of (possibly modified) arguments, or null to leave them unchanged. This one trims whitespace from customer names before an account is created, whatever channel creates it — storefront, admin or API.
<?php
declare(strict_types=1);
namespace MageServices\Demo\Plugin;
use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\Data\CustomerInterface;
class NormaliseCustomerNames
{
public function beforeCreateAccount(
AccountManagementInterface $subject,
CustomerInterface $customer,
$password = null,
$redirectUrl = ''
): array {
$customer->setFirstname(trim((string) $customer->getFirstname()));
$customer->setLastname(trim((string) $customer->getLastname()));
return [$customer, $password, $redirectUrl];
}
}
Example 3: An around Plugin
An around plugin receives a callable $proceed. Calling it runs the next plugin in the chain and eventually the original method. If you do not call it, the original method does not run at all — which is powerful and dangerous.
<?php
declare(strict_types=1);
namespace MageServices\Checkout\Plugin;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
use Magento\Checkout\Api\ShippingInformationManagementInterface;
use Magento\Framework\Exception\LocalizedException;
use MageServices\Checkout\Model\Config;
class BlockRestrictedPostcodes
{
public function __construct(
private readonly Config $config
) {
}
/**
* @throws LocalizedException
*/
public function aroundSaveAddressInformation(
ShippingInformationManagementInterface $subject,
callable $proceed,
$cartId,
ShippingInformationInterface $addressInformation
) {
$postcode = (string) $addressInformation->getShippingAddress()->getPostcode();
if ($this->config->isRestrictedPostcode($postcode)) {
throw new LocalizedException(__('Sorry, we cannot deliver to this postcode.'));
}
return $proceed($cartId, $addressInformation);
}
}
This particular rule could also be a before plugin, because it only needs to stop the call. That is usually the better choice: around plugins add stack depth to every call and make debugging harder. Use around only when you need code both before and after the original, or must skip it conditionally.
Plugin Order and sortOrder
When several plugins observe the same method, Magento sorts them by sortOrder (lower first). Before methods run in ascending order. Around methods wrap each other in ascending order, so the lowest sortOrder is the outermost. After methods run as each wrapped call returns, which means the plugin with the lowest sortOrder gets the final say on the result.
Disabling or Replacing Another Plugin
<type name="Vendor\Module\Model\Something">
<plugin name="vendor_plugin_name" disabled="true"/>
</type>
What You Cannot Plug Into
| Not supported | Use instead |
|---|---|
final classes and methods | A preference for the interface, or an event |
| Private and protected methods | Plug into the public method that calls them |
Static methods and __construct | Constructor arguments via di.xml, or a factory plugin |
| Virtual types | Plug into the real class the virtual type is based on |
| Objects created before Magento\Framework\Interception is initialised | An event or a preference |
Performance Tips
- Keep plugin code fast — some methods, such as product getters, are called thousands of times per page.
- Prefer interfaces (service contracts) over concrete classes.
- Scope plugins to the area that needs them, for example
etc/frontend/di.xml. - Inject heavy dependencies as proxies (
Vendor\Class\Proxy) when the plugin only uses them occasionally. - Run
bin/magento dev:di:info "Magento\Catalog\Model\Product"to list all plugins on a class.
Frequently Asked Questions
Plugin or observer — which should I use?
Use a plugin to change a method's arguments or result. Use an observer to react to something that happened, such as an order being placed, when an event is dispatched for it.
Why is my plugin not called?
Check the method is public and not final, the class name in di.xml is exact, the area is right, and run bin/magento setup:di:compile or clear generated/code in developer mode.
Plugin or preference?
Prefer plugins. A preference replaces the whole class, so only one module can use it and upgrades are more likely to break it.
Need help with this on your store?
Our Magento engineers can implement it for you, review your code or take on the whole project.
Written by the Magento Services engineering team — Magento 2, Adobe Commerce and Hyvä specialists since 2014. We write about problems we solve on real client stores.