Categories


Archives


Recent Posts


Categories


Magento 2 Object Manager Plugin System

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

We’re finally here. After our long path through the object manager, automatic constructor dependency injection, class preferences, argument replacement, virtual types, proxy objects and code generation, we’re finally familiar enough with the object system to discuss the true replacement for Magento 1’s class rewrites — the plugin system.

No lollygagging about, let’s get to it.

Installing the Module

Like all our tutorials we’ve prepared a boilerplate Magento module to get us started. Grab the latest tagged releases and manually install it into your system. If you’re not sure how to manually install a module, our first article in the series will set you right.

If you can successfully run the following command

$ php bin/magento ps:tutorial-plugin-installed
You've installed Pulsestorm_TutorialPlugin

you’re ready to start.

Magento 1 Class Rewrites

Magento 1 has a feature called “class rewrites”. This feature allows module developers to swap out the implementation of model, helper, or block class methods with their own class and method definitions. In Magento 1, each of these object types is created with a factory method

Mage::getModel('cms/page');    
Mage:helper('cms');
Mage::getModel('core/layout')->createBlock('cms/page');

Each of the strings above is a unique identifier for a specific class. For example, by default a cms/page model corresponds to the class Mage_Cms_Model_Page. Magento’s class rewrite system lets module developers say

Hey Magento — whenever you see a cms/page model, instantiate a Mynamespace_Mymodule_Model_Some_Other_Page object instead of a Mage_Cms_Modl_Page

Then, a module developer defines their own class to extend the original

class Mynamespace_Mymodule_Model_Some_Other_Page extends Mage_Cms_Page_Model
{
    //redefine any method here
}

In this way, the developer can redefine any method in the class they wanted to.

Class rewrites are popular because they allow a very specific redefinition of system functionality.

There are, however, some problems with class rewrites. One is a verbose Magento 1 XML configuration syntax that makes it easy to misconfigure a rewrite. The second is each class is only “rewritable” by a single module. Once one module claims a method, that means other modules are out of luck. This creates conflicts between modules that need to be fixed manually by someone with deep knowledge of both Magento and the two conflicting rewrites.

More than any specific issue though — the problem class rewrites solve (giving PHP a “duck-typing/monkey-patching system”) is not the problem they were used to solve (allowing third party developers to create modules and extensions that could listen for to and change any part of the Magento system).

As this series has shown, old style rewrites still exist in the form of class preferences and argument replacement — but Magento 2’s plugin systems sets out to solve the actual problem extension developers needed solved.

Magento Plugins

Magento’s plugin system allows you, a module/extension developer, to

If you’ve never built a system like this before, you may hear listen to any method call and start having nightmares about performance characteristics. Fortunately, the Magento 2 core team has members who have built systems like this before, and they know exactly which software design patterns to apply to make this work. We’ll touch a little on the interceptor patten in use here, but a full discussion is outside the scope of this article.

Our Program

We’re going to jump right in and create a new Magento plugin. Assuming you’ve installed the sample module, try running the following command.

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Result: hello hola!

If you’re still with us after all this time, this code should be self explanatory. If we jump to the command’s source

#File: app/code/Pulsestorm/TutorialPlugin/Command/Testbed.php
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);
}

we see our command fetched the class name for $this->example using the get_class function. The $example property is a standard automatic constructor dependency injection parameter

#File: app/code/Pulsestorm/TutorialPlugin/Command/Testbed.php    
public function __construct(Example $example)
{
    $this->example = $example;
    return parent::__construct();
}

Then, the command calls this Example object’s getMessage method with two parameters, and outputs the result.

The

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage

text comes from the definition of getMessage in Pulsestorm\TutorialPlugin\Model\Example.

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example.php    
public function getMessage($thing='World', $should_lc=false)
{
    echo "Calling the real " . __METHOD__, "\n";
    $string = 'Hello ' . $thing . '!';
    if($should_lc)
    {
        $string = strToLower($string);
    }
    return $string;
}

The purpose of this debugging code will become clear in a moment.

What we have here is a pretty standard, if silly, example program. If you’re having trouble following along, you might want to review the series so far.

Creating the Plugin

We want to create a Magento 2 Plugin for the getMessage method of the Pulsestorm\TutorialPlugin\Model\Example class. It’s inevitable that the larger community will start using the word “plugin” as a place-holder for “extension” or “module”, but in the language of the object manager, a “plugin” refers to a special class that’s listening for any public method call to another object.

We say any public method, but that’s not quite accurate. If the public method uses the final keyword, or the class itself uses the final keyword, you won’t be able to use a plugin. Outside of that restriction, any class and public method is fair game, including your classes, Magento core classes, and third party classes.

To create a plugin, add the following configuration to your di.xml file

#File: app/code/Pulsestorm/TutorialPlugin/etc/di.xml
<config>   
    <!-- ... -->

    <type name="Pulsestorm\TutorialPlugin\Model\Example">
        <plugin name="pulsestorm_tutorial_plugin_model_example_plugin" 
                type="Pulsestorm\TutorialPlugin\Model\Example\Plugin" 
                sortOrder="10" 
                disabled="false"/>
    </type>
</config>

Once again, we’re creating a <type/> configuration. The name of the type should be the class whose behavior you’re trying to change.

Under type is the <plugin/> node. Here, the name should be a unique identifier that distinguishes this plugin configuration from every other plugin configuration in the world. The name is freeform, but you should use some combination of module name and description to make sure the name is unique.

The type of the plugin is a PHP class name. This PHP class will be your plugin class, and where we’ll define our custom behavior. We’ll want to create this class as well, but for now lets leave it empty of actual methods. To do so, create the following class file in the following location

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php
<?php
namespace Pulsestorm\TutorialPlugin\Model\Example;
class Plugin
{
}

Magento 2 convention dictates that this class’s short name should be \Plugin, although any class name will do.

Jumping back to di.xml, the sortOrder attribute controls how your plugin interacts with other plugins on the same class — we’ll talk more about this later.

A disabled attribute of true allows you to leave a plugin configuration in your di.xml, but have Magento ignore it. We include it here for completeness’s sake.

The above is all we need for a fully configured plugin. It’s a plugin that doesn’t do anything, but it’s a plugin nonetheless. Before we continue, lets clear our cache

$ php bin/magento cache:clean
Cleaned cache types:
config
layout
block_html
collections
db_ddl
eav
full_page
translate
config_integration
config_integration_api
config_webservice

and try running our program again.

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Result: hello hola!

You’ll notice the behavior of getMessage remains the same. However, the object manager no longer returns a Pulsestorm\TutorialPlugin\Model\Example object when we ask for a Pulsestorm\TutorialPlugin\Model\Example type. Instead, we’re now dealing with an interceptor class (Pulsestorm\TutorialPlugin\Model\Example\Interceptor).

While a full explanation is beyond the scope of this article, the interceptor pattern is how Magento has implemented their plugin system — and this is one of the side effects of that pattern. Whenever you see an interceptor class, you’re dealing with a class under observation by a Magento plugin.

The class itself (Pulsestorm\TutorialPlugin\Model\Example\Interceptor) is automatically generated by Magento, and can be found in the var/generation folder. This is the same sort of code generation we saw with proxy objects.

#File: var/generation/Pulsestorm/TutorialPlugin/Model/Example/Interceptor.php
class Interceptor extends \Pulsestorm\TutorialPlugin\Model\Example implements \Magento\Framework\Interception\InterceptorInterface
{
    //...
}

As you can see, the generated intercepter extends the original Pulsestorm\TutorialPlugin\Model\Example class, which means the interceptor object will behave the same as the original Pulsestorm\TutorialPlugin\Model\Example (except where it’s been extended to work with the plugin system). At the end of the day you shouldn’t need to think about interceptors. They’re just a class that sits between the programmer (you) and the original class we’re “plugging” into. Interceptors are what allow Magento’s plugin system to work.

Speaking of which, let’s take a look at exactly what plugins will let you do.

Plugins: After Methods

As mentioned previously, a Magento 2 plugin is sort of like an observer for class method calls. We’re going to start with an after plugin method that the system will call after the getMessage method is called. Add the following method to your empty Plugin class

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php
<?php
namespace Pulsestorm\TutorialPlugin\Model\Example;
class Plugin
{
    public function afterGetMessage($subject, $result)
    {
        echo "Calling " , __METHOD__,"\n";
        return $result;
    }    
}

and then run the command again.

$ php bin/magento ps:tutorial-plugin

We're going to calls the `getMessage` method on the class 

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::afterGetMessage
Result: hello hola!

If you did everything correctly, you should see the debugging output

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::afterGetMessage

Congratulations — you just implemented your first Magento after plugin method.

An after plugin method gets its name by concatenating the word after to the actual method name, with the whole thing camel cased.

'after' + 'getMessage' = 'afterGetMessage'

An after plugin method has two parameters. The first ($subject above), is the object the method was called on (in our case, that’s Pulsestorm\TutorialPlugin\Model\Example\Interceptor object). The second parameter ($result above) is the result of the original method call.

Since we returned $result above, our after plugin method didn’t change any behavior. Let’s try actually changing something. Edit your afterGetMessage method so it matches the following

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
public function afterGetMessage($subject, $result)
{        
    return 'We are so tired of saying hello';
} 

Run the command with the above in place, and you should see the following

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::afterGetMessage
Result: We are so tired of saying hello

Congratulations! You just used a plugin to change the value returned by the getMessage method.

Plugins: Before Methods

While the after plugin methods should be your go-to method for hooking into system behavior, they’re not appropriate for every situation. Let’s take a look at our getMessage call again

#File: app/code/Pulsestorm/TutorialPlugin/Command/Testbed.php
$result = $this->example->getMessage("Hola", true);

You’ll notice we used a hard coded string as an argument here, which means, even with systems like automatic constructor dependency injection, there’s no way to change this argument before the getMessage method gets called.

This sort of problem is what the before plugin methods can solve. Let’s try changing our Plugin class to match the following. (removing our after plugin method for simplicity’s sake)

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
<?php
namespace Pulsestorm\TutorialPlugin\Model\Example;
class Plugin
{   
    public function beforeGetMessage($subject, $thing='World', $should_lc=false)
    {
        echo "Calling " . __METHOD__,"\n";
    }        
}

Run the command with the above in place, and you should see the following output

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::beforeGetMessage
Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Result: hello hola!

That is, our debugging text in beforeGetMessage is printed out before the call to the real getMessage.

Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::beforeGetMessage
Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage

Similar to after plugin methods, the before plugin method naming convention is the word before concatenated with a camel case version of the original method, and the first parameter is the original object ($subject above). However, unlike the after plugin method, there’s no $result parameter. That’s because (in retrospect, obviously) we haven’t called our real method yet, so there can’t be a return/result value.

A before plugin method does have additional parameters.

public function beforeGetMessage($subject, $thing='World', $should_lc=false)

These second, third (fourth, fifth, etc) parameters are a copy of the original method parameters ($thing and $should_lc above). These should match the parameters from the original method definition, including default values.

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example.php
public function getMessage($thing='World', $should_lc=false)
{
    echo "Calling the real " . __METHOD__, "\n";
    $string = 'Hello ' . $thing . '!';
    if($should_lc)
    {
        $string = strToLower($string);
    }
    return $string;
}

The reason these parameters are here is a before plugin method will let you change the values of the arguments before passing them along to the real method. You do this by returning an array of new arguments. Give the following a try

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
public function beforeGetMessage($subject, $thing='World', $should_lc=false)
{
    return ['Changing the argument', $should_lc];
}  

The code above replaces the first argument ($thing) with the string 'Changing the argument'. It also passes on the value of $should_lc without changing it. Run our command with the above in place, and you should see the new argument (Changing the argument) used in the actual method call to getMessage.

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Result: hello changing the argument!    

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 method.

The before plugin methods are slightly more dangerous that the after plugin methods. By changing the value of an argument, you may change the behavior of existing code in an undesirable way, or uncover a bug in a method that previously hadn’t surfaced. You’ll want to use extra caution when using before plugin methods, and make absolutely sure there isn’t another way to achieve your goal.

Plugins: Around Methods

There’s one last plugin listener type to cover, and that’s the around plugin methods. The before methods fire before, the after methods fire after, and the around methods fire during, or as a replacement to the original method.

If that’s a little confusing, an example should set us straight. Make your plugin code match the following (again, removing our previous before plugin method for the sake of simplicity)

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php
<?php
namespace Pulsestorm\TutorialPlugin\Model\Example;
class Plugin
{
    public function aroundGetMessage($subject, $procede, $thing='World', $should_lc=false)
    {
        echo 'Calling' . __METHOD__ . ' -- before',"\n";
        $result = $procede();
        echo 'Calling' . __METHOD__ . ' -- after',"\n";
        return $result;
    }  
}

If we run our program the debugging echo calls should make the execution order clear.

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::aroundGetMessage -- before
Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Example\Plugin::aroundGetMessage -- after
Result: Hello World!

The around plugin methods give you the ability, in a single place, to have code run both before the original method, and after the original method. How it does this is in the magic of the second parameter

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
public function aroundGetMessage($subject, $procede, $thing='World', $should_lc=false)

This second parameter (named $procede above) is an anonymous function (i.e. a PHP Closure). If you, as a plugin developer, are using an around plugin method, you call/invoke this closure

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
$result = $procede();

when you want the system to call the original method.

This is a powerful technique. It also means you can cancel the call to the original method, and substitute your own return value. Give the following a try.

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
<?php
namespace Pulsestorm\TutorialPlugin\Model\Example;
class Plugin
{
    public function aroundGetMessage($subject, $procede, $thing='World', $should_lc=false)
    {
        echo 'Calling' . __METHOD__ . ' -- before',"\n";
        //$result = $procede();
        echo 'Calling' . __METHOD__ . ' -- after',"\n";
        return 'New return value';
    }  
}

Run our program with the above in place, and you should see the following

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

CallingPulsestorm\TutorialPlugin\Model\Example\Plugin::aroundGetMessage -- before
CallingPulsestorm\TutorialPlugin\Model\Example\Plugin::aroundGetMessage -- after
Result: New return value

You’ll notice that both the result to our getMessage method call has changed, and we’ve lost the debugging code that let us know the real getMessage ran.

While the around plugin methods give you the power to completely replace a piece of system functionality, this also means that you’re responsible for making sure your new code is a suitable replacement for the code you’re replacing. While every developer and every team will need to forge their own path here, speaking for myself I’ll be treading lightly on the around plugin methods.

Playing Nice with Others

While the implementation is different, the object manager’s plugin system allows developers to do most of what the old class rewrite system did. For example — in Magento 1 an around plugin method situation might look something like this

#File: app/code/Pulsestorm/TutorialPlugin/Model/Example/Plugin.php    
<?php

class Namespace_Module_Model_Someclass extends Mage_Core_Model_Someclass
public function someMethod($foo, $baz, $bar)
{
    $this->_doSomeBeforeStuff($foo, $baz, $bar);
    $result = parent::someMethod($foo, $baz, $bar);
    $result = $this->_doSomeAfterStuff($result);
    return $result;        
}

The difference with plugins is, since we’re not actually inheriting from the original class, we

  1. Con: Lose access to calling protected methods on $subject
  2. Pro: Avoid accidentally changing the behavior of the subject
    class by redefining methods

However, where plugins really distinguish themselves from class rewrites is in the system’s ability to mesh together plugins that observe the same method. Magento 1 is rife with real world conflicts where two modules/extensions try to rewrite the same class, with confusing and disastrous results.

We’ve prepared a quick example of this. Let’s change our di.xml file so it looks like this (removing the above plugin configuration)

<!-- File: app/code/Pulsestorm/TutorialPlugin/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- START: argument replacement that enables our command -->
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="Pulsestorm\TutorialPluginTestbedCommand" xsi:type="object">Pulsestorm\TutorialPlugin\Command\Testbed</item>
            </argument>
        </arguments>
    </type>    
    <!-- END: argument replacement that enables our command -->

    <!-- START: two new plugins -->
    <type name="Pulsestorm\TutorialPlugin\Model\Example">
        <plugin name="pulsestorm_tutorial_plugin_model_conflict_plugin1" 
                type="Pulsestorm\TutorialPlugin\Model\Conflict\Plugin1" 
                sortOrder="10" 
                disabled="false"/>
    </type>

    <type name="Pulsestorm\TutorialPlugin\Model\Example">
        <plugin name="pulsestorm_tutorial_plugin_model_conflict_plugin2" 
                type="Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2" 
                sortOrder="15" 
                disabled="false"/>
    </type>       
    <!-- START: two new plugins -->
</config>

Here, we’ve configured two plugins on the Pulsestorm\TutorialPlugin\Model\Example class, and each plugin has an after plugin method defined for the getMessage method.

#File: app/code/Pulsestorm/TutorialPlugin/Model/Conflict/Plugin1.php
public function afterGetMessage($subject, $result)
{
    echo "Calling " , __METHOD__ , "\n";
    return 'From Plugin 1';
}

#File: app/code/Pulsestorm/TutorialPlugin/Model/Conflict/Plugin2.php    
public function afterGetMessage($subject, $result)
{
    echo "Calling " , __METHOD__ , "\n";
    return 'From Plugin 2';
}

If we run our program with the above configuration (after clearing our cache), we’ll see the following output.

$ php bin/magento ps:tutorial-plugin

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

    Pulsestorm\TutorialPlugin\Model\Example\Interceptor

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin1::afterGetMessage
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2::afterGetMessage
Result: From Plugin 2

There’s two interesting things here. First — the Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2 plugin “won” the return value game. Unlike other systems/patterns (Drupal’s hooks, for example), Magento’s plugin system still has a winner-take-all situation when it comes to return values. The reason Plugin2 “won” is because it had a higher sortOrder (15 vs. 10)

<!-- File: app/code/Pulsestorm/TutorialPlugin/etc/di.xml -->
<plugin name="pulsestorm_tutorial_plugin_model_conflict_plugin1" 
        type="Pulsestorm\TutorialPlugin\Model\Conflict\Plugin1" 
        sortOrder="10" 
        disabled="false"/>

<plugin name="pulsestorm_tutorial_plugin_model_conflict_plugin2" 
        type="Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2" 
        sortOrder="15" 
        disabled="false"/>

However, if we look at our debugging methods

Calling the real Pulsestorm\TutorialPlugin\Model\Example::getMessage
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin1::afterGetMessage
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2::afterGetMessage

We see that Plugin1 still has its after plugin method called. This means that state and behavioral system changes due to other plugin code still happen, but that one plugin’s results still win out.

Here’s another bit of interesting behavior. Change the two plugin methods so they match the following

#File: app/code/Pulsestorm/TutorialPlugin/Model/Conflict/Plugin1.php 
public function afterGetMessage($subject, $result)
{
    echo "Calling " , __METHOD__ , "\n";    
    echo "Value of \$result: " . $result,"\n";
    return 'From Plugin 1';
}

#File: app/code/Pulsestorm/TutorialPlugin/Model/Conflict/Plugin2.php     
public function afterGetMessage($subject, $result)
{
    echo "Calling " , __METHOD__ , "\n"; 
    echo "Value of \$result: " . $result,"\n";   
    return 'From Plugin 2';
}

And then run the program

$ php bin/magento ps:tutorial-plugin

//...
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin1::afterGetMessage
Value of $result: hello hola!
Calling Pulsestorm\TutorialPlugin\Model\Conflict\Plugin2::afterGetMessage
Value of $result: From Plugin 1
Result: From Plugin 2

As you can see, each after plugin method is aware of the return value from the other plugin.

While not fool proof, the plugin system greatly reduces the surface area for potential extension conflicts, and gives end users a pure configuration approach (sortOrder) for solving conflicts on their own.

The Magento “Public API”

There’s one last thing to cover w/r/t the object manager’s plugin system, something that’s more political than it is technical. If you’ve been looking at Magento 2 source code, you may notice that certain class methods have an @api doc block annotation

#File: app/code/Magento/Widget/Model/Widget.php
/**
 * Return widget config based on its class type
 *
 * @param string $type Widget type
 * @return null|array
 * @api
 */
public function getWidgetByClassType($type)
{
    //...
}

This @api isn’t something that affects the running of your program. Instead, it’s a documentation flag from the Magento core team that says

We promise this method will be there in future versions of Magento 2

While I can’t speak for other developers — after half a decade plus of building Magento extensions this sort of “public API” is a welcome addition to the platform. Knowing which methods you can safely use in a plugin, or even just call, is a big win for extension developers, and should help tremendously with the “new release of Magento causes fatal errors in my extension/module” problem.

As to whether all extension developers respect this public API setting or not, that should be an “An Interesting Technically Obtuse Challenge™”.

Caveats and Wrap up

Before we wrap things up, there’s a few random caveats to mention that we don’t have time to dive deeply into today. Feel free to ask about the items below in the comments here, or at the Magento Stack Exchange if it’s technically in-depth.

First, the interceptor class that makes this all possible is generated code, which means if the class you’re observing with a plugin changes — you’ll need to regenerate the interceptor.

Next, if you fail to call the $procede() closure in an around plugin method, not only are you skipping a method call to the original class method, you’re also skipping any plugins that have a higher sort order. I’m not wild about this tradeoff, and I expect it to be a cause of consternation among early extension developers.

Next, with plugins (vs. rewrites) you lose the ability to change the behavior of a protected method. Some will see this as a con (less flexibility), and some will see this a pro (respecting the protected access levels). It you need to change the behavior of a protected method, class preferences or argument replacement are more suitable tools.

Finally, there’s no great rule of thumb for tracking a return value through the various before, around, and after methods, and if you’re deep in the weeds doing this your first step should probably be reconsidering a plugin as your solution. For the stubborn masochists, Christof/FoomanNZ has a series of tests that dive into this, and the code that runs through all configured plugins calling before, around, and after methods is found here

#File: lib/internal/Magento/Framework/Interception/Interceptor.php    
protected function ___callPlugins($method, array $arguments, array $pluginInfo)
{
    //...
}    

And with that, this series is brought to a close. Magento 2’s big bet on Enterprise Software Design Patterns™ has less to do with what the community will do with Magento, and more to do with wrangling a large development team towards the unified goal of a Q4 2015 Magento 2 release. While it’s important that you understand that basics of how the object manager and Magento’s automatic constructor dependency injection system works — it’s up to you to decide how much of your own code will mirror the patterns in Magento, or if these patterns will be the industrial scaffolding you hang your own, simpler code on.

Originally published September 2, 2015
Series Navigation<< Magento 2 Object Manager: Instance Objects