Magento 2 interceptors under PHP 7

So first of all the great news: It has been announced that Magento 2.2 (the upcoming release) will support only PHP 7. In other words, PHP 5 support is dropped and we can move our code forward to PHP 7. So what this mean for Magento 2 interceptors.

Quick introduction of interceptors

Interceptors are a really cool thing in Magento 2: They allow an original class to be modified by another class, by appending information before, after or around the original methods. This is done by creating a module with a di.xml file that defines a type for the original class and adds a plugin to it. Next, the plugin class is defined. For instance, the original Product class might have a setName() method that looks as follows:

/**
 * Set product name
 *
 * @param string $name
 * @return $this
 */
public function setName($name)
{
    return $this->setData(self::NAME, $name);
}

Now our plugin might modify this method using a plugin method beforeSetName() simply to trim the argument $name before passing it to setName(). The code looks like this:

public function beforeSetName(Product $subject, $name)
{
    $name = trim($name);
    return [$name];
}

Under the hood, the original class and the plugin class are (more-or-less) compiled into a new class called an interceptor.

The danger of using plugins

As you can see, the original setName() method of the original Product class has an argument $name. And the PHPDoc arguments tell us that this argument is supposed to be a string. Likewise the return argument is supposed to return $this. However, there is nothing in the code that prevents us from changing this. This makes plugins dangerous.

They allow you to modify the original input arguments and the original return value, without the original class knowing about it. And when problems arise, they will be really hard to troubleshoot because the end result - the interceptor - will have the bug. Unit tests for the original class will simply work.

Do not change the signature!

The real danger of a plugin lies in its nature: It's allowed to change the signature of an original method without issues. It shouldn't. But it can. What I have been telling developers during my trainings is therefore to make sure to never break the signature of the original methods. If the input argument is a string, do make sure to keep it a string.

This is just a convention. In PHP 7 we have a much better solution for this: Argument typing and return typing.

Adding argument typing and return typing

In PHP 7 we can modify the code and add type hints so that the code defines itself what kind of signature should be in place. In the code sample below, we have added a string argument type, plus a array return type. Effectively this prevents us from breaking the original method:

public function beforeSetName(Product $subject, string $name): array
{
    $name = trim($name);
    return [$name];
}

You can already make this modification in Magento 2.0 and 2.1 environments. Just realize that you will need PHP 7 to run this code, because it breaks in PHP 5 (while using PHP 5 breaks common sense).

Note that the return type of before-methods might be different. Sometimes it might be a single return value (if there is only 1 input argument on the original method). However, it sometimes requires a return of an array. It depends.

Add strict typing

We can also take this a step further. Instead of just using PHP 7 typing, we can also add a declare(strict_types=1) statement in the beginning of our PHP file, which will enforce these PHP 7 types to be validated more strictly: An integer might still be casted into a string without the strict mode.

declare(strict_types=1)

class ProductPlugin
{
    public function beforeSetName(Product $subject, string $name): array
    {
        $name = trim($name);
        return [$name];
    }
}

Peeking at the original class

And of course, the original code should be modified as well: It might be updated to add argument types and return types. And because this information is added to the PHP code itself, it makes no sense to duplicate the information in the PHPDoc section either, so we can remove it there. The cleaned-up example would look like this:

/**
 * Set product name
 */
public function setName(string $name): Product
{
    return $this->setData(self::NAME, $name);
}

It might be that - after the release of Magento 2.2 - the Magento core-team will make work out of it to modify all of the original code in a similar way as above, maybe even enforcing the strict mode. However, it might also not be coming that soon. If you want to help, you can. Simply create Pull Requests for adding type declarations. At least we can embrace PHP 7 completely in our own code already.