- Magento 2 Object Manager
- Magento 2’s Automatic Dependency Injection
- Magento 2 Object Manager Preferences
- Magento 2 Object Manager Argument Replacement
- Magento 2 Object Manager Virtual Types
- Magento 2 Object Manager: Proxy Objects
- Magento 2 Object Manager: Instance Objects
- Magento 2 Object Manager Plugin System
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 aMynamespace_Mymodule_Model_Some_Other_Page
object instead of aMage_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
- “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
- Do so while other modules are doing the same thing to the same method in a sane/predictable way
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 return
ed $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
- Con: Lose access to calling protected methods on
$subject
- 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.