What You Cannot Intercept

Interception generates a subclass, so it only reaches public, non-static, non-final instance methods. Everything else is silently ignored.

Interception works by generating a subclass of the target. That one fact explains every limit: if a subclass could not override the method, a plugin cannot wrap it.

The list

  • final methods, and every method of a final class.
  • Methods that are not public. private and protected are both out of reach.
  • static methods.
  • __construct and __destruct.
  • Virtual types, which are named DI configurations of an existing class declared with virtualType. Put the plugin on the real class.
  • Objects created before the interception framework is bootstrapped, which includes the object manager itself and what it needs to start.
  • Classes implementing Magento\Framework\ObjectManager\NoninterceptableInterface, which every generated Proxy does.

What trips people up

None of these produce an error. Magento does not warn that the method you named cannot be wrapped: the plugin class loads, the method is simply never called. People edit the body, flush caches and recompile for an hour before checking the target's visibility.

The usual cause is an underscore-prefixed method. In Magento\Framework\View\Element\AbstractBlock, toHtml() is public and can be intercepted, while _toHtml() beside it is protected and cannot. When a plugin does nothing at all, open the target class and read the method's modifiers first, then confirm with bin/magento dev:di:info after bin/magento cache:clean config.

Example

Example · php
<?php
// app/code/Vendor/Pricing/Plugin/ClampRounding.php
declare(strict_types=1);

namespace Vendor\Pricing\Plugin;

use Vendor\Pricing\Service\Rounder;

/**
 * Target: Vendor\Pricing\Service\Rounder
 *
 *   public function __construct(int $precision = 2)   constructor: cannot intercept
 *   public function round(float $amount): float       public: can intercept
 *   final public function roundUp(float $amount)      final: cannot intercept
 *   protected function apply(float $amount): float    not public: cannot intercept
 *   public static function zero(): float              static: cannot intercept
 */
class ClampRounding
{
    /**
     * @param Rounder $subject
     * @param float $result
     * @return float
     */
    public function afterRound(Rounder $subject, float $result): float
    {
        return $result < 0.0 ? 0.0 : $result;
    }
}

// Output: round(-1.004) returns 0.0
// Output: roundUp(-1.004) returns -1.0, because final methods are never wrapped

When to use it

  • Work out why a plugin that looks right never runs.
  • Decide where to attach a plugin when the target is reached through a virtual type.
  • Change a constructor dependency without inheritance, after discovering constructors cannot be intercepted.

More examples

A plugin on a virtual type does nothing

A virtual type gives an existing class a name plus different constructor arguments; no new PHP class exists for Magento to extend. The cost of moving the plugin to the real class is that it now applies to every use of that class, not just this one, so keep such plugins cheap. Apply with bin/magento cache:clean config.

Example · xml
<!-- app/code/Vendor/Log/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="vendorLogHandler" type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/vendor.log</argument>
        </arguments>
    </virtualType>

    <!-- Ignored: a virtual type is a configuration, not a class to subclass -->
    <type name="vendorLogHandler">
        <plugin name="vendor_log_wrong" type="Vendor\Log\Plugin\TagLine"/>
    </type>

    <!-- Works: declare it on the real class -->
    <type name="Magento\Framework\Logger\Handler\Base">
        <plugin name="vendor_log_right" type="Vendor\Log\Plugin\TagLine"/>
    </type>
</config>

Proxies are not interceptable, their subjects are

The Proxy suffix asks Magento to generate a lazy stand-in so the real object is built only when first used. Generated proxies implement NoninterceptableInterface, so the stand-in itself is never wrapped; the Builder it eventually creates is an interceptor, so the plugin still runs. Declare the plugin on the real class name, never on the Proxy name.

Example · xml
<!-- app/code/Vendor/Report/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Vendor\Report\Console\Command\Export">
        <arguments>
            <argument name="builder" xsi:type="object">Vendor\Report\Service\Builder\Proxy</argument>
        </arguments>
    </type>

    <type name="Vendor\Report\Service\Builder">
        <plugin name="vendor_report_tag_builder" type="Vendor\Report\Plugin\TagBuilder"/>
    </type>
</config>

When the constructor is what you wanted to change

Interception cannot reach __construct, so configure construction instead. A di.xml argument changes a scalar or swaps an injected dependency for another class; a preference replaces the class outright. Both are applied with bin/magento cache:clean config, and in production mode with setup:di:compile.

Example · xml
<!-- 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">
    <type name="Vendor\Pricing\Service\Rounder">
        <arguments>
            <argument name="precision" xsi:type="number">4</argument>
        </arguments>
    </type>
</config>

Discussion

  • Be the first to comment on this lesson.