around, and Why to Avoid It
An around plugin receives a callable for the rest of the chain, costs more than before or after, and is only justified when you must stop the call.
public function aroundFoo($subject, callable $proceed, $arg)An around plugin runs in place of the observed method. It receives the subject, then a callable (by convention named $proceed), then the method's own arguments. Calling $proceed() runs the next plugin in the chain, or the original method if none is left. Code before the call happens on the way in, code after it on the way out.
Why it is discouraged
Adobe's documentation is direct: avoid around methods when they are not required, because they increase stack traces and affect performance. Every one of them adds another closure around the call. The one case the documentation names as appropriate is when the execution of all further plugins and of the original method needs to be terminated.
The example is that case. While a configuration flag is on, any action extending Magento\Framework\App\Action\Action returns a redirect and its execute() never runs. A request to /checkout/cart/ answers 302 to the home page instead of rendering the cart.
What goes wrong
Forgetting $proceed silently removes the feature you were extending: the original method never runs and the caller gets whatever your plugin returned, often null. Passing the wrong arguments to $proceed is the same bug in a quieter form.
Two forms spell the same call: $proceed(...$args) and call_user_func_array($proceed, $args). The spread is the shorthand.
Declaring any plugin is a di.xml change, so apply it with bin/magento cache:clean config.
Example
<?php
// app/code/Vendor/Gate/Plugin/CloseStorefront.php
declare(strict_types=1);
namespace Vendor\Gate\Plugin;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\ResultFactory;
class CloseStorefront
{
private const XML_PATH_CLOSED = 'vendor_gate/general/closed';
public function __construct(
private readonly ScopeConfigInterface $scopeConfig,
private readonly ResultFactory $resultFactory
) {
}
/**
* Target: Magento\Framework\App\Action\Action::dispatch()
*
* @param Action $subject
* @param callable $proceed
* @param RequestInterface $request
* @return mixed
*/
public function aroundDispatch(Action $subject, callable $proceed, RequestInterface $request)
{
if (!$this->scopeConfig->isSetFlag(self::XML_PATH_CLOSED)) {
return $proceed($request);
}
return $this->resultFactory
->create(ResultFactory::TYPE_REDIRECT)
->setPath('cms/index/index');
}
}When to use it
- Stop a controller from running while the store is behind a feature flag.
- Replace a third-party method's behaviour outright because its result cannot be corrected afterwards.
- Convert an inherited around plugin into before and after methods during a performance pass.
More examples
The forgotten callable
This is the single most common interception bug and it has no error message. If a whole area of the store goes blank after a deployment, look for an around plugin before anything else.
<?php
// app/code/Vendor/Audit/Plugin/TimeDispatch.php
declare(strict_types=1);
namespace Vendor\Audit\Plugin;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\RequestInterface;
use Psr\Log\LoggerInterface;
class TimeDispatch
{
public function __construct(private readonly LoggerInterface $logger)
{
}
// Wrong: no call to $proceed. Every page in the store returns an
// empty response, and the log line still looks correct.
public function aroundDispatch(Action $subject, callable $proceed, RequestInterface $request)
{
$this->logger->debug('dispatch', ['path' => $request->getPathInfo()]);
}
}The same work as an after plugin
Whenever an around plugin always calls $proceed, it can be rewritten as a before method, an after method, or one of each. Prefer that: the stack stays shallow and there is no way to forget the callable. Reach for around only when you need to stop the call.
<?php
// app/code/Vendor/Audit/Plugin/TimeDispatch.php
declare(strict_types=1);
namespace Vendor\Audit\Plugin;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\RequestInterface;
use Psr\Log\LoggerInterface;
class TimeDispatch
{
public function __construct(private readonly LoggerInterface $logger)
{
}
public function afterDispatch(Action $subject, $result, RequestInterface $request)
{
$this->logger->debug('dispatched', ['path' => $request->getPathInfo()]);
return $result;
}
}Passing arguments through without naming them
Useful when you wrap a method whose signature you expect to gain parameters, because a variadic plugin keeps forwarding whatever it was handed. It also hides mistakes, so name your arguments whenever you can.
<?php
// app/code/Vendor/Audit/Plugin/PassThrough.php
declare(strict_types=1);
namespace Vendor\Audit\Plugin;
class PassThrough
{
public function aroundSomeMethod($subject, callable $proceed, ...$args)
{
// Shorthand and longhand for the same call:
$result = $proceed(...$args);
// $result = call_user_func_array($proceed, $args);
return $result;
}
}
Discussion