Magento 2 Object Manager Plugin System

Like this article? 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.

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::getMode;('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

  • “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 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 <br/> 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

Magento 2 Object Manager: Instance Objects

Like this article? 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.

It’s been a long and winding road, but the end is in sight! In this, the penultimate article in our object manager tutorial, we’re going to discuss working with instance and non-injectable objects in Magento 2.

This article assumes a basic familiarity with Magento 2 concepts, and may brush past something complicated. If you’re confused you may want to start from the beginning, or use the comment system below to point to your Magento Stack Exchange question.

Let’s get to it!

Sample Code

We’ve prepared a small sample Magento 2 module that we’ll reference and use in this article. The module’s on GitHub, and if you’re not familiar with manually installing Magento 2 modules the first article in this series has a good overview for installing a module via one of the tagged releases

To test that you’ve installed the module correctly, try running the following command

$ php bin/magento ps:tutorial-instance-objects
You've installed Pulsestorm_TutorialInstanceObjects!

If you see the You've installed Pulsestorm_TutorialInstanceObjects! message, you’re all set.

Shared/Unshared, Singleton/Instance

Back in our first article, we introduced two object manager methods for instantiating objects

$object  = $manager->create('Pulsestorm\TutorialInstanceObjects\Model\Example');
$object  = $manager->get('Pulsestorm\TutorialInstanceObjects\Model\Example');

The create method will instantiate a new object each time it’s called. The get method will instantiate an object once, and then future calls to get will return the same object. This behavior is similar to Magento 1’s getModel vs. getSingleton factories

Mage::getModel('group/class');      // ->create, or instance object
Mage::getSingleton('group/class');  // ->get,    or instance object

What we didn’t cover was how the automatic constructor dependency injection system decides which method to use when it encounters a constructor parameter. Consider a constructor that looks like this.

//...
use Pulsestorm\TutorialInstanceObjects\Model\Example;
//...
public function __construct(Example $example)
{
    //is $example created with `get` or `create`?
    $this->example = $example?
}

We know the parameter will be a Pulsestorm\TutorialInstanceObjects\Model\Example object, but we don’t know if it will be a new instance of a Pulsestorm\TutorialInstanceObjects\Model\Example object, or the same Pulsestorm\TutorialInstanceObjects\Model\Example object passed/injected into other constructors.

By default, all objects created via automatic constructor dependency injection are “singleton-ish” objects — i.e. they’re created via the object manager’s get method.

If you want a new instance of an object, i.e. you want the object manager to use create, you’ll need to add some additional <type/> configuration to your module’s di.xml file.

If we wanted the object manager to instantiate Pulsestorm\TutorialInstanceObjects\Model\Example as an instance object every time, we’d add the following configuration to our di.xml

<!-- File: app/code/Pulsestorm/TutorialInstanceObjects/etc/di.xml --> 
<config>
    <!-- ... -->
    <type name="Pulsestorm\TutorialInstanceObjects\Model\Example" shared="false">
        <!-- ... arguments/argument tags here if you want to change injected arguments -->
    </type>
</config>

This is the same <type/> tag we saw back in our argument replacement tutorial. The name attribute should be the name of the class whose behavior you want to change.

The new-to-us attribute is shared. If shared is set to false, then Magento 2 will use the create method to instantiate an object every time it encounters Pulsestorm\TutorialObjectManager1\Model\Example as an automatically injected constructor argument. The shared attribute has no effect on objects instantiated directly via PHP’s new keyword, or the object manager’s two methods.

This attribute is named shared due to an implementation detail in the object manager. When you use get to instantiate an object, the object manager stores all the already instantiated objects in a _sharedInstances array.

#File: lib/internal/Magento/Framework/ObjectManager/ObjectManager.php
public function get($type)
{
    $type = ltrim($type, '\\');
    $type = $this->_config->getPreference($type);
    if (!isset($this->_sharedInstances[$type])) {
        $this->_sharedInstances[$type] = $this->_factory->create($type);
    }
    return $this->_sharedInstances[$type];
}

When you configure a specific type (i.e. a specific PHP class) with shared="false", you’re telling Magento 2 that you don’t want to use this _sharedInstances array.

So, all you need to remember here is shared="true" is the default, and you’ll get a singleton-ish/global object. If you change your type configuration to shared="false", the automatic constructor dependency injection system will start instantiating a new parameter every-time a programmer instantiates the attribute’s owner object.

Magento 2 Factories

While the shared attribute is a useful bit of duct tape for those times you need an injected dependency to be a brand new instance object, it’s not an ideal solution for every (or even most) cases where you don’t want singletons.

One problem with shared is the injected dependency is still dependent on its owner object being shared or un-shared. There’s lots of times where you just need a new instance of an object and might not be able to refactor your object relationships to accomplish that.

A perfect example of this are CRUD data objects, such as Magento’s CMS page objects or the catalog product objects. In Magento 1 you’d create a CMS page object like this

Mage::getModel('page/cms')->load($id);

In Magento 2, these sorts of objects are called “non-injectables”. Dependency injection is meant for objects that “do some thing”, or “provide some service”. These data objects, however, are meant to “identify this specific thing”. What we’re going to look at next is how to use these sorts of objects without automatic constructor dependency injection tying our hands.

First, there’s nothing stopping you from directly instantiating an object via PHP’s new method

$product = new \Magento\Cms\Model\Page;

However, if you were to do this, your CMS Page object loses out on all of Magento’s object manager features.

Fortunately, the Magento 2 core developers haven’t abandoned us. In Magento 2, you instantiate these sorts of non-injectable objects via factory objects. Like so much in Magento 2, an example is worth 1,000 words.

#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php

public function __construct(
    \Magento\Cms\Model\PageFactory $pageFactory = 
)
{
    $this->pageFactory = $pageFactory;
    return parent::__construct();
}
//...
public function execute(InputInterface $input, OutputInterface $output)
{
    $page = $this->pageFactory->create();
    foreach($page->getCollection() as $item)
    {
        $output->writeln($item->getId() . '::' . $item->getTitle());
    }

    $page = $this->pageFactory->create()->load(1);        
    var_dump($page->getData());
}

Here’s what’s going on: In the __constructor method, the command uses Magento’s automatic constructor dependency injection to create a Magento\Cms\Model\PageFactory object, and then (per Magento convention) assigns that object to the pageFactory property.

#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
public function __construct(
    \Magento\Cms\Model\PageFactory $pageFactory = 
)
{
    $this->pageFactory = $pageFactory;
}

Then, in execute, we use this factory object to create a CMS page object (using the factory’s create method)

$page = $this->pageFactory->create();    

In Magento 1, the above is roughly equivalent to

$page = Mage::getModel('cms/page');

In Magento 1, we had a set of factory methods (getModel, ‘helper’, ‘createBlock’). In Magento 2 — every non-injectable object has its own factory object.

You can find the factory for any model class by appending the text Factory to that model class’s name. Above we wanted to instantiate Magento\Cms\Model\Page objects, so the factory class was

Object we Want: Magento\Cms\Model\Page
Factory to Use: Magento\Cms\Model\PageFactory

Similarly, here’s the two classes for a product object

Object we Want: Magento\Catalog\Model\Product         
Factory to Use: Magento\Catalog\Model\ProductFactory

Once we use a factory to instantiate a class, we have most (if not all) of our old Magento 1 CRUD methods available to us (load, getData, getCollection, etc.)

#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
$page = $this->pageFactory->create();
foreach($page->getCollection() as $item)
{
    $this->output($item->getId() . '::' . $item->getTitle());
}

$page = $this->pageFactory->create()->load(1);        

This may take a little getting used to, but compared to the error prone XML configuration needed to use Magento 1’s factory methods, this is already a big win.

Factory Definitions and Code Generation

There’s one last thing to cover about factory objects in Magento 2 that might answer a few questions in your head

  • Ugh! I need to define a bunch of boiler plate factory code? Lame.
  • Where can I see what a factory object looks like?

If we start with the second and more adult question, you may be in for a small surprise. Based on the factory’s full class name, (Magento\Cms\Model\PageFactory), you might expect to find it at one of the following locations

app/code/Magento/Cms/Model/PageFactory.php
lib/internal/Magento/Cms/Model/PageFactory.php

However, neither of these files exist.

That’s because Magento 2 uses automatic code generation to create factory classes. You may remember this code generation from the proxy object article. If you’ve actually run the code above, you’ll find the PageFactory class in the following location.

var/generation/Magento/Cms/Model/PageFactory.php

While the specifics are beyond the scope of this article, whenever

  • PHP encounters a class name that ends in Factory
  • And the autoloader can’t load that class because there’s no definition file

Magento 2 will automatically create the factory.

If you’re curious how this happens this Stack Exchange answer showing the Magento/Framework/Code/Generator/Autoloader kickoff point is a good place to start.

Factories for All

Factories aren’t just for Magento core code — they’ll work with any module class. The sample module we had you install includes a Pulsestorm\TutorialInstanceObjects\Model\Example object. Let’s replace the __construct method with one that adds a factory class for the Example object.

//...
use Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory;
//...
class Testbed extends Command
{
    protected $exampleFactory;
    public function __construct(ExampleFactory $example)
    {
        $this->exampleFactory = $example;
        return parent::__construct();
    }
}

Then, we’ll use that factory in execute.

protected function execute(InputInterface $input, OutputInterface $output)
{
    $example = $this->exampleFactory->create();
    $output->writeln(
        "You just used a"                . "\n\n    "
        get_class($this->exampleFactory) . "\n\n" . 
        "to create a \n\n    "           . 
        get_class($example) . "\n"); 
}

Run our command with the above execute method in place, and you should see the following.

$ php bin/magento ps:tutorial-instance-objects
You just used a

    Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory

to create a 

    Pulsestorm\TutorialInstanceObjects\Model\Example

As you can see, this code ran without issue, despite our never defining a Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory class. You can find the factory definition in the generated code folder

#File: var/generation/Pulsestorm/TutorialInstanceObjects/Model/ExampleFactory.php
<?php
namespace Pulsestorm\TutorialInstanceObjects\Model;

/**
 * Factory class for @see \Pulsestorm\TutorialInstanceObjects\Model\Example
 */
class ExampleFactory
{
    protected $_objectManager = null;

    protected $_instanceName = null;

    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager, 
        $instanceName = '\\Pulsestorm\\TutorialInstanceObjects\\Model\\Example'
    )
    {
        $this->_objectManager = $objectManager;
        $this->_instanceName = $instanceName;
    }

    public function create(array $data = array())
    {
        return $this->_objectManager->create($this->_instanceName, $data);
    }
}

As for the specific implementation — right now a factory’s create method accepts an array of parameters and users the object manager to create the object. However, future versions of Magento may change how these factories work. By having the framework generate these factories Magento 2 saves us the error prone busy work of coding up this boilerplate and the core team maintains control over how the factories work.

Wrap Up

With this article and its six predecessors complete, we have a pretty good understanding of Magento’s object system, how that system impacts the shape of the Magento code base, and (most importantly) the basic understanding that’s critical if we want to apply reason to Magento 2 code in the real world.

There is, however, one last thing we need to cover, and that’s the object plugin system. The plugin system is the true successor to Magento 1’s class rewrite system, and with a solid understanding of the object manager and automatic constructor dependency injection, we’re ready to tackle this next time in our final Object Manager tutorial.

Originally published August 30, 2015

Magento 2 Object Manager: Proxy Objects

Like this article? 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.

With their take on an object manager/container and dependency injection, Magento have created a new way for PHP programmers to work. With that new way of working comes a new set of unanticipated challenges. Put another way, new patterns create new problems.

Fortunately, Magento 2’s extended development cycle has given the core team time to address some of these problems with — you guessed it — more design patterns and more object manager features.

Today we’re going to take a look the problem of ill-performant code while using other people’s objects. Along the way we’ll discuss Magento 2’s use of the proxy pattern to solve this problem, as well as our first real discussion of Magento 2’s code generation features.

We’re deep in the weeds here — while you may find something of value as a newcomer, you’ll definitely want to get familiar with the basics of Magento’s object system to get the full value out of the article.

Installing the Module

Rather than have you spend 30 minutes copying and pasting code, we’ve created two modules that simulate a common problem you’ll run into with Magento automatic constructor dependency injection. The modules (Pulsestorm_TutorialProxy1 and Pulsestorm_TutorialProxy2) are available on GitHub

The official installation procedure for a Magento module is still being worked out, so we recommend installing these tutorial modules manually using the latest tagged release. If you’re not sure how to install a module manually, the first article in this series has the instructions you’re looking for.

To test that you’ve installed the module correctly, try running the following command

$ php bin/magento ps:tutorial-proxy
You've installed Pulsestorm_TutorialProxy2!
You've also installed Pulsestorm_TutorialProxy1!

If you see output telling you you’ve installed both modules, you’re ready to go!

Slow Loading Dependencies

These two modules simulate a common situation when using other people’s code — you’d like to make use of the their objects, but their code includes slow loading dependencies that you don’t need.

Let’s open up the command class and comment out our install check and uncomment the other lines

#File: app/code/Pulsestorm/TutorialProxy2/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    //$this->installedCheck($output);
    $service = $this->createService($output);        
    $this->sayHelloWithFastObject($service, $output);
    $this->sayHelloWithSlowObject($service, $output);
}

The first thing this code does ($this->createService($output);), is create a Pulsestorm\TutorialProxy1\Model\Example object using the object manager.

#File: app/code/Pulsestorm/TutorialProxy2/Command/Testbed.php
protected function createService($output)
{
    //...
    $om = $this->getObjectManager();
    //...
    $service = $om->get('Pulsestorm\TutorialProxy1\Model\Example');
    //...
    return $service;    
}

We’re using the object manager directly here for simplicity’s sake — in real Magento 2 code you’ll create most of your objects via automatic constructor dependency injection. Since automatic constructor dependency injection uses the object manager, everything we show you today will be available to your injected objects as well.

After instantiating that object, we call the two sayHello... methods

#File: app/code/Pulsestorm/TutorialProxy2/Command/Testbed.php
protected function sayHelloWithFastObject($service, $output)
{
    //...
    $service->sayHelloWithFastObject();
    //...
}

protected function sayHelloWithSlowObject($service, $output)
{
    //...
    $service->sayHelloWithSlowObject();
    //...        
}

All these methods do is pass on method calls to the Pulsestorm\TutorialProxy1\Model\Example object. Also, and omitted above, these methods contain some simple profiling code that will let us know how long each method took to run. We’ve also dropped similar profiling code in the Example service object and its dependencies.

If we run our command, we should see output something like the following

$ php bin/magento ps:tutorial-proxy
About to Create Service
Constructing FastLoading Object
Constructing SlowLoading Object
Created Service, approximate time to load: 3005.656 ms

About to say hello with fast object
Hello
Said hello with fast object, approximate time to load: 0.0172 ms

About to say hello with slow object
Hello
Said hello with slow object, approximate time to load: 0.0491 ms

While we’ll eventually get to all the output, the line we care about right now is this one

Created Service, approximate time to load: 3005.656 ms

Something is taking over 3 seconds (3000 milliseconds) to load. In our example, this three second load is simulated using a PHP sleep statement — but in the real world slow loading constructors can be murder to objects musing dependency injection.

Digging Deeper

Let’s take another look at our execute method

#File: app/code/Pulsestorm/TutorialProxy2/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    //this->installedCheck($output);
    $service = $this->createService($output);        
    $this->sayHelloWithFastObject($service, $output);
    $this->sayHelloWithSlowObject($service, $output);
}

At this level of abstraction, all we’re doing is

  • Creating a Pulsestorm\TutorialProxy1\Model\Example object
  • Calling that object’s sayHelloWithSlowObject and sayHelloWithFastObject methods

If we take a quick look at the source for our Example object

#File: app/code/Pulsestorm/TutorialProxy1/Model/Example.php
namespace Pulsestorm\TutorialProxy1\Model;
//...
public function __construct(FastLoading $fast, SlowLoading $slow)
{
    $this->fast = $fast;
    $this->slow = $slow;
}

We see it has two dependencies — Pulsestorm\TutorialProxy1\Model\FastLoading and Pulsestorm\TutorialProxy1\Model\SlowLoading. In turn, the sayHelloWithFastObject and sayHelloWithSlowObject methods

#File: app/code/Pulsestorm/TutorialProxy1/Model/Example.php
public function sayHelloWithFastObject()
{
    $this->fast->hello();
}

public function sayHelloWithSlowObject()
{
    $this->slow->hello();
}  

pass on a call to the hello method of the fast and slow arguments (now stored as object properties).

We’ve also added profiling to

  1. The creation of the Example object
  2. The calling of both sayHelloWithFastObject and sayHelloWithSlowObject

We’ve also added some debugging messages to the constructors of our FastLoading and SlowLoading classes.

#File: app/code/Pulsestorm/TutorialProxy1/Model/FastLoading.php
public function __construct()
{
    echo "Constructing FastLoading Object","\n";
}

#File: app/code/Pulsestorm/TutorialProxy1/Model/SlowLoading.php
public function __construct()
{
    echo "Constructing SlowLoad Object","\n";
    //...
}

Finally, in the __construct method of the SlowLoading class, we’ll see the following

#File: app/code/Pulsestorm/TutorialProxy1/Model/SlowLoading.php

public function __construct()
{
    echo "Constructing SlowLoading Object","\n";
    //simulate slow loading object with sleep
    sleep(3);
}

That is — we’ve placed a three second sleep in the constructor to simulate a slow loading object.

The end result is, when we run our command, we have a detailed report of what’s going on, and how long it’s taking.

$ php bin/magento ps:tutorial-proxy
About to Create Service
Constructing FastLoading Object
Constructing SlowLoading Object
Created Service, aproximate time to load: 3000.7651 ms

About to say hello with fast object
Hello
Said hello with fast object, approximate time to load: 0.0162 ms

About to say hello with slow object
Hello
Said hello with slow object, approximate time to load: 0.052 ms

Based on the above, it’s taking approximately three seconds to create a Pulsestorm\TutorialProxy1\Model\Example object. We now have a crude, if exaggerated, version of the sort of performance problem you might run into in the real world.

Why so Slow?

The first question we need to answer is

Why is the Example class loading slowly?

Based on what we’ve learned in this article so far, it’s the SlowLoading objects that are our slow ones, not the Example object.

The problem here is Magento’s automatic constructor dependency injection system. If we take another look at the Example object’s constructor

#File: app/code/Pulsestorm/TutorialProxy1/Model/Example.php
namespace Pulsestorm\TutorialProxy1\Model;
//...
public function __construct(FastLoading $fast, SlowLoading $slow)
{
    $this->fast = $fast;
    $this->slow = $slow;
}

We see two objects injected. Per Magento’s automatic constructor dependency injection, we know this means before the object manager instantiates a Pulsestorm\TutorialProxy1\Model\Example object, it will need to instantiate both a Pulsestorm\TutorialProxy1\Model\FastLoading and Pulsestorm\TutorialProxy1\Model\SlowLoading object.

So, our problem isn’t really a slow loading Example object — it’s a slow loading Pulsestorm\TutorialProxy1\Model\SlowLoading argument. This distinction might seem trivial, but it will be important in the solution Magento’s object manager provides.

Magento 2 Proxy Objects

The Magento core team must have run into this sort of a problem a lot — because not only do they have a solution for it, but that solution is baked into the object manager.

Magento’s solution is an implementation of the proxy pattern, used here to defer the loading of an object’s arguments.

If that didn’t make sense, don’t worry, we’ll guide you through the process step by step, and then explain what we did. If that did make sense to you, you’ll still want to proceed carefully — there’s some Magento 2 specific magic that, at first glance, makes everything a little confusing. However, once you’ve created a few proxy objects the magic should start making sense.

As previously discussed, our problem is

When Magento uses a Pulsestorm\TutorialProxy1\Model\SlowLoading object as an argument in a Pulsestorm\TutorialProxy1\Model\Example object, this slows down the instantiation of the Pulsestorm\TutorialProxy1\Model\Example object.

The solution to this is, via Magento’s di.xml configuration, to replace the Pulsestorm\TutorialProxy1\Model\SlowLoading argument with a proxy object. The proxy object will not suffer from the same slow loading as the original argument. Said another way, we’re going to use plain old argument replacement to replace the problem object with a different, special object called a proxy.

To do this, add the following nodes to the Pulsestorm_TutorialProxy2 module’s di.xml file.

#File: app/code/Pulsestorm/TutorialProxy2/etc/di.xml
@highlightsyntax@xml
<config>
    <!-- #File: app/code/Pulsestorm/TutorialProxy2/etc/di.xml -->
    <!-- Notice: we're using `TutorialProxy2`'s di.xml to change the
         behavior of `TutorialProxy1`.  This the far more common usage
         of object manager/dependency-injection than the examples in 
         our tutorial so far -->

    <type name="Pulsestorm\TutorialProxy1\Model\Example">
        <arguments>
            <argument name="slow" xsi:type="object">Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy</argument>
        </arguments>        
    </type>
</config>

This is (mostly) standard argument replacement. We’re targeting the arguments in the Pulsestorm\TutorialProxy1\Model\Example object, specifically the argument named slow, and replacing it with a Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy object.

The one new thing is the Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy class/object. We’re going to need to do a little hand waving and say “trust us” on what this is. All you need to know for now is, since we’re replacing the Pulsestorm\TutorialProxy1\Model\SlowLoading class, we replace it with a Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy class (i.e. — we append \Proxy to the initial class name).

If you clear your cache

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

and re-run the command with the above in place, you should see the following.

$ php bin/magento ps:tutorial-proxy
About to Create Service
Constructing FastLoading Object
Created Service, aproximate time to load: 6.0711 ms

//...

That is — we’ve gone from 3000 ms to around 6 ms. I’d say that’s a substantial improvement. Run the command again though

$ php bin/magento ps:tutorial-proxy
About to Create Service
Constructing FastLoading Object
Created Service, aproximate time to load: 0.736 ms

//...

And you’ll see an even more dramatic improvement. Your times may vary in their specificity, but the ratios/pattern should be similar. Congratulations! You’ve successfully used a proxy to improve the construction time of a Magento object manager object.

What Just Happened

Of course, this raises myriad questions. A few might be

  1. We never defined a Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy class, so what is it? A virtualType? Something else?

  2. The proxy pattern is about substituting one object for another — why does this improve performance?

  3. Why did we need to run the command twice to see the full performance improvements?

All valid questions, and they all get to the heart of the magic involved in Magento’s proxy objects.

The answer to the first question: The Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy configuration is not a virtual type — it’s a plain old PHP class. The reason we didn’t need to create this class on our own is, Magento automatically generates proxy class files for us. Take a look at the following file

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php    
<?php
namespace Pulsestorm\TutorialProxy1\Model\SlowLoading;

/**
 * Proxy class for @see \Pulsestorm\TutorialProxy1\Model\SlowLoading
 */
class Proxy extends \Pulsestorm\TutorialProxy1\Model\SlowLoading
{
    //...
}

This is the Pulsestorm\TutorialProxy1\Model\SlowLoading\Proxy class we configured, and Magento generated. Notice it’s in the var/generation folder.

When Magento’s object manage encounters a class whose “short name” is Proxy (i.e. whose full class name Ends\In\Proxy), it will automatically generate a proxy class like this one.

If you don’t believe us, try deleting this file

$ rm var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
$ ls -l var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
ls: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php: No such file or directory

and then re-run the command

$ php bin/magento ps:tutorial-proxy
//...
$ ls -l var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
-rw-rw-rw-  1 alanstorm  staff  2350 Aug  9 16:32 var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php        

The file’s been restored after running our command! It’s not just di.xml configuration that will trigger Magento code generation — it’s anytime the object Manager encounters a class whose short name is Proxy. Give the following a try — add the following temporary code to our execute method

#File: app/code/Pulsestorm/TutorialProxy2/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{

    $object_manager = $this->getObjectManager();
    $object = $object_manager->create('Pulsestorm\TutorialProxy1\Model\Example\Proxy');
    $output->writeln('You just instantiated a ' . get_class($object) . ' class'); 
    $output->writeln('You also indirectly told Magento to create this class in the following location');
    $r = new \ReflectionClass($object);
    $output->writeln($r->getFilename());        
    exit;  
    //...
}

and then run it.

$ php bin/magento ps:tutorial-proxy
You just instantiated a Pulsestorm\TutorialProxy1\Model\Example\Proxy class
You also indirectly told Magento to create this class in the following location
/path/to/magento/var/generation/Pulsestorm/TutorialProxy1/Model/Example/Proxy.php

So, above we told the object manager to create a Pulsestorm\TutorialProxy1\Model\Example\Proxy object. The object manager, seeing that the class name ended in Proxy, automatically created a proxy object that extends Pulsestorm\TutorialProxy1\Model\Example

#File: var/generation/Pulsestorm/TutorialProxy1/Model/Example/Proxy.php
<?php
namespace Pulsestorm\TutorialProxy1\Model\Example;

/**
 * Proxy class for @see \Pulsestorm\TutorialProxy1\Model\Example
 */
class Proxy extends \Pulsestorm\TutorialProxy1\Model\Example
{
}    

Be careful though, if you try to create a proxy for an object that doesn’t exist,

$object_manager->create('Pulsestorm\TutorialProxy1\Model\Ghost\Proxy');

the object manager will yell at you.

$ php bin/magento ps:tutorial-proxy

[Magento\Framework\Exception\LocalizedException]
Source class "\Pulsestorm\TutorialProxy1\Model\Ghost" 
for "Pulsestorm\TutorialProxy1\Model\Ghost\Proxy" generation does not exist.  

While PHP frameworks have used automatic code generation for a while, it’s usually at the explicit request of a user (i.e. “make me a file”). In Magento 2, code generation exists to save a developer the time of writing a lot of the boiler plate code that accompanies “design patterns” style programming.

This leads in well to our next topic: Why does a proxy object improve performance? Before we continue, don’t forget to remove the temporary object manager code from the execute method.

Magento’s Generated Proxy Objects

From Wikipedia — the proxy pattern

in its most general form, is a class functioning as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate.

If we take a look at the generated code, we’ll immediately see why a proxy object gave our object instantiation the performance boost it did.

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
<?php
namespace Pulsestorm\TutorialProxy1\Model\SlowLoading;

class Proxy extends \Pulsestorm\TutorialProxy1\Model\SlowLoading
{
    //...
    public function __construct(\Magento\Framework\ObjectManagerInterface $objectManager, $instanceName = '\\Pulsestorm\\TutorialProxy1\\Model\\SlowLoading', $shared = true)
    {
        $this->_objectManager = $objectManager;
        $this->_instanceName = $instanceName;
        $this->_isShared = $shared;
    }
    //...
}

The proxy object completely replaces the constructor of the proxied object, and does not make a parent::__construct call.

Proxy objects (like all objects in Magento) are still subject to dependency injection, and all proxy objects will have the same three arguments

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
public function __construct(\Magento\Framework\ObjectManagerInterface $objectManager, $instanceName = '\\Pulsestorm\\TutorialProxy1\\Model\\SlowLoading', $shared = true)
{
    $this->_objectManager = $objectManager;
    $this->_instanceName = $instanceName;
    $this->_isShared = $shared;
}

The first is an object manager instance, the second is the name of the class this object proxies, and the third is a shared argument. We’ll cover shared/unshared in a future article — for now just concentrate on the first two arguments.

So, by replacing the entire constructor, we avoid any slow loading behavior.

However — what if important things happen in the __construct method? What about the other properties assigned there? Aren’t we breaking the Pulsestorm\TutorialProxy1\Model\SlowLoading object by doing this?

Fortunately not. In addition to replacing the constructor, our proxy object also replaced each public method of the original object.

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
/**
 * {@inheritdoc}
 */
public function hello()
{
    return $this->_getSubject()->hello();
}

If someone calls hello, the proxy will call _getSubject and pass on the call to hello. The _getSubject method

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
protected function _getSubject()
{
    if (!$this->_subject) {
        $this->_subject = true === $this->_isShared
            ? $this->_objectManager->get($this->_instanceName)
            : $this->_objectManager->create($this->_instanceName);
    }
    return $this->_subject;
}

will instantiate an instance of the original object! Remember, the _instanceName variable argument from the constructor? If we use some x-ray vision, this method actually looks like

#File: var/generation/Pulsestorm/TutorialProxy1/Model/SlowLoading/Proxy.php
protected function _getSubject()
{
    if (!$this->_subject) {
        $this->_subject = true === $this->_isShared
            ? $this->_objectManager->get('\\Pulsestorm\\TutorialProxy1\\Model\\SlowLoading')
            : $this->_objectManager->create('\\Pulsestorm\\TutorialProxy1\\Model\\SlowLoading');
    }
    return $this->_subject;
}

In this way, Magento defers the loading of the slow loading object until it’s needed. In fact, if you re-examine our current output in its entirety.

$ php bin/magento ps:tutorial-proxy
About to Create Service
Constructing FastLoading Object
Created Service, aproximate time to load: 0.89 ms

About to say hello with fast object
Hello
Said hello with fast object, approximate time to load: 0.0069 ms

About to say hello with slow object
Constructing SlowLoading Object
Hello
Said hello with slow object, approximate time to load: 3001.014 ms

you’ll notice we still have a 3 second (3000 ms) delay, it’s just been deferred until we actually need the SlowLoading object

About to say hello with slow object
Constructing SlowLoading Object
Hello
Said hello with slow object, approximate time to load: 3001.014 ms

So, in our silly example program, the proxy doesn’t save us much. In the real world, you’ll use proxy objects in situations where

  1. You have a slow loading dependency
  2. You know your particular use of this code doesn’t need the dependency

Regardless of whether you choose to use proxies — they’re a part of the Magento 2 core system, and likely to show up in other people’s code, so make sure you understand them.

Generation Caveats

So, that’s questions one and two answered. As for the third

Why did we need to run the command twice to see the full performance improvements?

Some readers will have already figured this out. While code generation means we avoid having to create all the boiler plate code in our proxy class over and over again, there is a small performance cost involved (around 6 ms on my development machine). Once the code is generated, it stays generated — Magento 2 will only generate a file if it can’t instantiate the request object.

So the first performance improvement

[THREE SECONDS HERE]
Created Service, aproximate time to load: 6.0711 ms

//...

was the proxy object skipping direct instantiation of the SlowLoading argument.

The second performance improvement

Created Service, aproximate time to load: 6.0711 ms
Created Service, aproximate time to load: 0.736 ms

was PHP not having to regenerate an already generated class.

There’s another gotcha to code generation. If you change the original proxied class, Magento 2 won’t automatically re-generate any new public methods. This will create unpredictable behavior as you’ll still be able to call the new method (since the proxy object extends the original class), but the new methods won’t instantiate the subject object, and will cary a different state.

During development it would be wise to periodically remove your generated folder

var/generation

to trigger re-generation of any code that needs it.

For production systems — I expect this generated code folder will be one of those challenges early adopters will wrestle with. Magento 2 appears to offer two compilation commands) that will pre-generate code for the entire system — but these commands also go above and beyond generating code. Also, for anyone into scalable systems, on-the-fly generated code sets off all sort of trigger warnings for running in a multiple-app/frontend server environment. While these problems will, no doubt, be sorted out over the coming months and year(s), it’s a challenging climb ahead for Magento 2 devops engineers.

Fortunately for us — we’re a series for programmers! Unfortunately for us, we’ve covered a lot of information today (proxies and code generation), and it’s probably a good idea to take a small breather. Next time our code generation exposure will come in handy as we explore shared/unshared objects, as well as Magento 2’s take on the age old factory pattern.

Originally published August 23, 2015