Magento 2: Creating a Plugin

In this tutorial, I will talk about a feature of Magento called plugins

Posted on July 28, 2017 in Magento2

Introduction to Plugin Pattern Design

Interception is a software design pattern which is used when there is some need to insert code dynamically without necessarily changing the original class behavior. This works by dynamically inserting code between the calling code and the target object.

The interception pattern in Magento is implemented via plugins. They provide the before, after, and around listeners, which help us extend the observed method behavior.

In this tutorial, I will cover the following topics:

  • Creating a plugin
  • Using the before listener
  • Using the after listener
  • Using the around listener

Please note: plugins cannot be created for just any class or method, as they do not work for the following:

  • Final classes
  • Final methods
  • The classes that are created without a dependency injection

Creating a new Module

Start by creating a new module named ‘Jeff_Plugin’ with following steps

create registration.php file


#app/code/Jeff/Plugin/registration.php

<?php
    \Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        'Jeff_Plugin',
        __DIR__
    );


create module.xml file


#app/code/Jeff/Plugin/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Jeff_Plugin" setup_version="0.0.1"/>
</config>

create di.xml file

We are going to use Magento 2 command line feature for the tutorial


#app/code/Jeff/Plugin/etc/di.xml

<?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\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="jeff_plugin_command_testbed" xsi:type="object">Jeff\Plugin\Command\Testbed</item>
            </argument>
        </arguments>
    </type>
</config>

create Testbed.php fiel


#app/code/Jeff/Plugin/Command/Testbe.php

<?php
namespace Jeff\Plugin\Command;

use Magento\Framework\ObjectManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Testbed extends Command
{
    protected $om;
    protected $example;
    
    public function __construct(ObjectManagerInterface $om, \Jeff\Plugin\Model\Example $ex) {
         $this->om = $om;
         $this->example = $ex;
         return parent::__construct();
    }
    protected function configure()
    {
        $this->setName("ps:plugin");
        $this->setDescription("The command for testing magento 2 plugins.");
        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln("Hello World");
    }
}

Create a test Model class Example.php


#app/code/Jeff/Plugin/Model/Example.php

<?php
namespace Jeff\Plugin\Model;

class Example {
    public function getMessage($thing='world', $should_lc=false) {
        $string = 'Hello ' . $thing . '!';
        if($should_lc) {
            $string = strToLower($string);
        }

        return $string;
    }
}

After following commands, you will make sure everything is working correctly


php bin/magento moduel:enable Jeff_Plugin
php bin/magento cache:clean
php bin/magento setup:upgrade

php bin/magento ps:plugin

Partial output looks like:

Hello World

Creating a plugin

Update di.xml file as following:


<?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\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="jeff_plugin_command_testbed" xsi:type="object">Jeff\Plugin\Command\Testbed</item>
            </argument>
        </arguments>
    </type>

    <type name="Jeff\Plugin\Model\Example">
        <plugin name="jeff_plugin_model_example_plugin" type="Jeff\Plugin\Model\Example\Plugin" sortOrder="10" disabled="false" />
    </type>
</config>

Plugins are defined within the module di.xml file. To define a plugin, by using the type elemeent and its name attribute, we first map the class that we want to observe. In this case, we are observing the \Jeff\Plugin\Model\Example class.
In the type element, we then define one ore more plugins using the plugin element.

The plugin element has the following four attributes assigned to it:

  • name: Using this attribute, you can provide a unique and recognizable name value that is specific to the plugin.
  • sortOrder: This attribute determines the order of execution when multiple plugins are observing the same method.
  • disabled: The default value of this attribute is set to default, but if it is set to true, it will disabled the plugin
  • type: This attribute points to the class that we will be using to implement the before, after, or around listener

create Plugin.php file


#app/code/Jeff/Plugin/Model/Example/Plugin.php

<?php
namespace Jeff\Plugin\Model\Example;

class Plugin {
    public function beforeGetMessage($subject, $thing="World", $should_lc=false) {
        echo "Calling " . __METHOD__ . "\n";

        return ['Changing the argument', $should_lc];
    }

    public function afterGetMessage($subject, $result) {
        echo "Calling ", __METHOD__, "\n";
        $result = $result . ' Something appending here. Hear from you.';
        return $result;
    }

    public function aroundGetMessage($subject, $procede, $thing="World", $should_lc=false) {
        echo 'Calling ' . __METHOD__ . ' -- before' ."\n";
        /*
            if you are using an around plugin method, you call/invoke this closure: $result = $procede(); notice the '$' sign
        */
        $result = $procede();

        echo 'Calling ' . __METHOD__ . ' -- after' . "\n";

        return $result;
    }
}

As you can see, we are observing before, after and around listeners for the getMessage()method which is defined in the \Jeff\Plugin\Model\Example class.

Update the Testbed.php


#app/code/Jeff/Plugin/Command/Testbed.php

<?php
namespace Jeff\Plugin\Command;

use Magento\Framework\ObjectManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Testbed extends Command
{
    protected $om;
    protected $example;

    public function __construct(ObjectManagerInterface $om, \Jeff\Plugin\Model\Example $ex) {
        $this->om = $om;
        $this->example = $ex;

        return parent::__construct();
    }
    protected function configure()
    {
        $this->setName("ps:plugin");
        $this->setDescription("The command for testing magento 2 plugins.");
        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln(
            "\nWe're going to call the `getMessage` method on the class " . "\n\n" .
            '    ' . get_class($this->example) . "\n"
        );

        $result = $this->example->getMessage("Hola", true);
        $output->writeln('Result: ' . $result);
    }
}

The results are shown following


run command:
php bin/magento ps:plugin

result:
We're going to call the `getMessage` method on the class 

    Jeff\Plugin\Model\Example\Interceptor

Calling Jeff\Plugin\Model\Example\Plugin::beforeGetMessage
Calling Jeff\Plugin\Model\Example\Plugin::aroundGetMessage -- before
Calling Jeff\Plugin\Model\Example\Plugin::aroundGetMessage -- after
Calling Jeff\Plugin\Model\Example\Plugin::afterGetMessage
Result: Hello world! Something appending here. Hear from you.

The Plugin class does not have to be extends from another class for the plugin to work. We define the before, after and around listeners for the getMessage() method by using the naming convention, as follows:


<before> + <getMessage> = beforeGetMessage
<after> + <getMessage> = afterGetMessage
<around> + <getMessage> = aroundGetmessage

An after plugin method has two parameters. The first is the object the method was called on (In our case, that’s Jeff\Plugin\Model\Example\Intercepter object). The second is the result of the original method call.

The before plugin method has three parameters, the first is the object the method was called on (Jeff\Plugin\Model\Example\Intercepter object), second and third parameters are the parameters for the observed method getMessage(). As you can see, in addition to being able to take programmatic action before a method is called, the before plugin methods let us change the arguments passed into the mesthod.

The around plugin method has four parameters, the first , third, fourth are the same as before plugin method. The second parameter is an anonymous function(PHP Closure). If you want to get result from the original method, we have to put this code: $rsult = $procede();. While the around plugin methods give you the power to completely replace a piece of system functionality, this also means that you are responsible for making sure your new code is a suitable replacement for the code you’re replacing.

Conclusion

In this tutorial, I showed you how to create a new module, a command line script for magento 2 and a plugin for listening before, after, and around observer for an original method.

Magento 2’s plugins system allows your to:

  • Listen to any method call made on an object manager controlled object and take programmatic action
  • Change the return value of any method call made on an object manager controlled object.
  • Change the arguments of any method call made to an object manager controlled object.

comments powered by Disqus