Interception In Magento 2

The aim of this article is to shed some light on interceptors in Magento 2 and give some insight into how they differ from observers. I will try to explain to the the best of my ability and understanding how they both work. I will also, somewhat briefly, provide a basic example of how you would go about integrating this.

Interception

Interception is not unique to Magento 2, it’s a programming pattern. You’re more than likely familiar with Object-oriented programming, however you may not be familiar with Aspect-oriented programming.

Aspect Oriented Programming helps increase modularization through-out an application, which you’ve hopefully noticed Magento 2 has done. By using a pointcut, we can access a specific point in our application and perform an action, this is very similar to event/observers in Magento. If you’d like to learn more about AOP I’d highly suggest watching Daniel Sloof’s talk on Aspect Oriented Magento.

The difference between interception and the observer pattern however is that we don’t need a specific event to listen to. I’m sure many of you have been in a situation in Magento 1 where you’ve need to change some functionality but there wasn’t an event being fired, this would lead to rewrites and other code smells.

Anton Krill describes Interception as “Ability observe public method calls in application”, which sums it up nicely. source here.

Interceptors can provide functionality:

  • Before a method
  • After a method
  • Around a method

Interception Integration In Magento

Okay, so first of all interceptors are referred to as plug-ins in Magento 2. As Magento 2 is a configuration based system, like always we define our plug-in within our XML config.

You will declare you plugin in di.xml, which makes sense.

path/to/your/module/etc/di.xml:

<config>
    <type name="Magento\Catalog\Model\Product">
        <plugin name="brideo_product_plugin" type="Brideo\Example\Model\Product\Plugin" sortOrder="100" disabled="false"/>
    </type>
</config>
  • Type Name: The class you want to intercept
  • Plugin Name: Your unique name
  • Plugin Type: The class name of your interceptor class or plug-in
  • Sort Order: Sets the priority should two modules conflict and be overriding the same method
  • Disabled: Is the plugin active

Okay now we have done this we can create our plugin class:

path/to/your/Model/Product/Plugin.php:

The method I would like to intercept is Magento\Catalog\Model\Product::getName() so I can return my own product name for whatever reason; to do this is very simple, check out the class:

    <?php

    namespace Brideo\Example\Model\Product;

    use Magento\Catalog\Model\Product;

    class Plugin
    {

        public function beforeGetName(Product $subject)
        {
            // perform some logging.
        }

    }

If you’d like to read more on integration I would recommend just checking the Dev Docs Magento plug-ins. They will be maintained to a much higher degree than this article will, also the docs cover this in more depth.

What is going on behind the scenes?

Like with Factory classes, Magento 2 auto-generates Interceptor classes on the fly within the var/generation directory.

The Interceptor class will use the Magento\Framework\Interception\Interceptor trait.

Example of a auto-generated class:

    <?php
    namespace Magento\Catalog\Model\Product;

    /**
     * Interceptor class for @see \Magento\Catalog\Model\Product
     */
    class Interceptor extends \Magento\Catalog\Model\Product implements \Magento\Framework\Interception\InterceptorInterface
    {
        use \Magento\Framework\Interception\Interceptor;

        /**
        * The dependencies would be injected and passed to the parent here
        * but I deleted them for readability.
        **
        */
        public function __construct()
        {
            $this->___init();
            parent::__construct();
        }

        /**
         * {@inheritdoc}
         */
        public function getName()
        {
            $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getName');
            if (!$pluginInfo) {
                return parent::getName();
            } else {
                return $this->___callPlugins('getName', func_get_args(), $pluginInfo);
            }
        }

    }

I deleted most of the functions in this class because it was huge, you only need to see one to understand what is going on. So Magento will instantiate this class which sits on top of the original class, it then auto-generates every single public function which belongs to the class (which I removed).

If the $pluginInfo variable is set, it will proceed to the ___callPlugins within the trait:

    /**
     * Calls plugins for a given method.
     *
     * @param string $method
     * @param array $arguments
     * @param array $pluginInfo
     * @return mixed|null
     */
    protected function ___callPlugins($method, array $arguments, array $pluginInfo)
    {
        $capMethod = ucfirst($method);
        $result = null;
        if (isset($pluginInfo[DefinitionInterface::LISTENER_BEFORE])) {
            // Call 'before' listeners
            foreach ($pluginInfo[DefinitionInterface::LISTENER_BEFORE] as $code) {
                $beforeResult = call_user_func_array(
                    [$this->pluginList->getPlugin($this->subjectType, $code), 'before'. $capMethod],
                    array_merge([$this], $arguments)
                );
                if ($beforeResult) {
                    $arguments = $beforeResult;
                }
            }
        }
        if (isset($pluginInfo[DefinitionInterface::LISTENER_AROUND])) {
            // Call 'around' listener
            $chain = $this->chain;
            $type = $this->subjectType;
            /** @var \Magento\Framework\Interception\InterceptorInterface $subject */
            $subject = $this;
            $code = $pluginInfo[DefinitionInterface::LISTENER_AROUND];
            $next = function () use ($chain, $type, $method, $subject, $code) {
                return $chain->invokeNext($type, $method, $subject, func_get_args(), $code);
            };
            $result = call_user_func_array(
                [$this->pluginList->getPlugin($this->subjectType, $code), 'around' . $capMethod],
                array_merge([$this, $next], $arguments)
            );
        } else {
            // Call original method
            $result = call_user_func_array(['parent', $method], $arguments);
        }
        if (isset($pluginInfo[DefinitionInterface::LISTENER_AFTER])) {
            // Call 'after' listeners
            foreach ($pluginInfo[DefinitionInterface::LISTENER_AFTER] as $code) {
                $result = $this->pluginList->getPlugin($this->subjectType, $code)
                    ->{'after' . $capMethod}($this, $result);
            }
        }
        return $result;
    }

Now, depending on the type mentioned earlier (before, after, around) it will fire your method with any appropriate arguments, this gives us the option to alter or log data at any given point.

In Conclusion

It’s my opinion integrating this pattern provides huge flexibility and helps us developers stick to the O in our SOLID principle; Open-Closed: “software entities … should be open for extension, but closed for modification.” - Wikipedia.

comments powered by Disqus