A Magento 2 plugin wraps one method. A Laravel decorator wraps the whole service. Same job, different size, and the Magento one has 3 edges worth knowing: one stops PHP, and 2 fail without a line in the logs.
How a Magento plugin works
In Magento 2 you declare a plugin in di.xml. Magento then generates an Interceptor into generated/code: a subclass of the original that runs your before, after or around code on one public method. Your code asks for the original class and gets the Interceptor.
Because the Interceptor is a subclass, PHP's own rules apply to it.
A plugin on a final class is a fatal error: "cannot extend final class". Without a compile it kills the first request that needs the class. With setup:di:compile it fails the build.
A plugin on a final or protected method does nothing. The generator only wraps public methods that aren't final or static. The original runs unchanged, with no error and no log line.
The trap: around without $proceed
An around plugin receives $proceed, the rest of the chain. If you don't call it, the original method doesn't run. Neither does any plugin with a higher sortOrder: an around plugin at sortOrder 10 that never calls $proceed silently skips a before plugin at 20. system.log and exception.log stay empty.
So keep around for 2 cases. One is wrapping the original, the way the core's CatalogSearch plugin puts a product save inside a transaction. The other is skipping it, and a cache hit is the classic one: the plugin below memoizes a slow call to an ERP. For everything else, prefer before or after.
<type name="Vendor\Erp\Api\StockClientInterface"> <plugin name="acme_memo_erp_stock" type="Acme\Stock\Plugin\MemoizeErpStock"/> </type> class MemoizeErpStock { private array $memo = []; // around, not after: a cache hit has to skip the HTTP call public function aroundGetQty( StockClientInterface $subject, callable $proceed, string $sku ): float { return $this->memo[$sku] ??= $proceed($sku); } } With this plugin, 3 calls to getQty() in one frontend request reach the ERP client once.
The plugin object is shared, so the memo lives as long as the PHP process. In a web request that's what you want. In a queue consumer it would keep serving the same stock figure for days. That's why the plugin is declared for the frontend area only: in the admin area the same 3 calls reach the client 3 times.
Laravel: decorate the whole service
Laravel doesn't have method interception. The closest tool is the container's extend(): you get the resolved service and return a decorator around it.
The trade-off is size. A plugin targets one method. A decorator implements the whole interface, so an interface with 10 methods means 10 methods in the decorator, and 9 of them only pass the call on. In return it's explicit: one line in a service provider and no generated code.
$this->app->extend(StockChecker::class, fn (StockChecker $inner) => new MemoizedStockChecker($inner)); // a decorator implements every method of the interface class MemoizedStockChecker implements StockChecker { private array $memo = []; public function __construct(private StockChecker $inner) {} public function qty(string $sku): float { return $this->memo[$sku] ??= $this->inner->qty($sku); } } Where I used it
On one Magento store I profiled 129 URLs with the full-page and block caches off, and built 58 fixes. 23 of them are plugin-level caches behind one small SharedCache helper, keyed by store, customer group and page context. 8 of those sit on the home page: the CMS page render, the CMS block widgets and the top menu among them. With all the fixes in, the home page went from 802 ms to 184 ms on the server.
The rule
Wrap the smallest thing that works. In Magento that's one method, with before or after unless you have to wrap or skip the original. In Laravel it's the service, and you pay for it with the whole interface.
Next: Magento's compiled DI next to Laravel's container, and a logger trap that writes to system.log too.
Have you ever found an around plugin that forgot to call $proceed?
Part 3 of 15 of the Magento 2 vs Laravel series. Originally published on LinkedIn.

