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

Magento 2 Object Manager Virtual Types

Last time we discussed the argument replacement feature of Magento 2’s object manager, and introduced the <type/> tag in di.xml. This week we’re going to talk about another feature related to argument replacement — Virtual Types.

Virtual types allow a module developer (you!) to create “configuration only classes” for the object manager’s argument replacement feature. If that didn’t make sense, don’t worry, by the end of this article you’ll understand how to read and trace <virtualType/> configurations, and have the information you’ll need to decide if they’re for you.

Warning: This article assumes you’ve been following along in our series, and have a general understanding of Magento’s automatic dependency injection feature, argument replacement, and the role of Magento’s object manager. This article makes direct use of the object manager to simplify concepts. In the real world Magento’s best practices dictate end-user-programmers (you!) not use the object manager directly. Instead you should rely on automatic constructor dependency injection for instantiating your objects. All the features we discuss will be available to objects created via automatic constructor dependency injection.

Additional Warning: Despite our attempts to simplify things, virtual types involve nested levels of automatic constructor dependency injection — if you’re having trouble grasping the concepts it’s not because you’re not smart, it’s because they’re complicated concepts and take time to understand.

Installing the Module

We’ve created a module with much of the boiler plate code you’ll need to get started with virtual types. The module is 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-virtual-type
Installed Pulsestorm_TutorialVirtualType!

Once you see the Installed Pulsestorm_TutorialVirtualType! output, you’re ready to start.

The Object Setup

This module’s main purpose is to setup a few object relationships. At the top of our composition hierarchy, we have a Pulsestorm\TutorialVirtualType\Model\Example object. This object’s class makes use of automatic constructor dependency injection

#File: app/code/Pulsestorm/TutorialVirtualType/Model/Example.php
<?php
namespace Pulsestorm\TutorialVirtualType\Model;
class Example
{
    public $property_of_example_object;
    public function __construct(Argument1 $the_object)
    {
        $this->property_of_example_object = $the_object;
    }
}

The automatic constructor dependency injection will inject a Pulsestorm\TutorialVirtualType\Model\Argument1 object, which is then assigned to the property named property_of_example_object.

In turn, this Pulsestorm\TutorialVirtualType\Model\Argument1 class also uses automatic constructor dependency injection.

#File: app/code/Pulsestorm/TutorialVirtualType/Model/Argument1.php
<?php
namespace Pulsestorm\TutorialVirtualType\Model;
class Argument1
{
    public $property_of_argument1_object;
    public function __construct(Argument2 $the_argument)
    {
        $this->property_of_argument1_object = $the_argument;
    }
}

This time the object manager will inject a Pulsestorm\TutorialVirtualType\Model\Argument2 object, which the constructor assigns to the object property named property_of_argument1_object.

In hierarchical terms, we have (using shorthand class names)

Example (contains a)
    Argument1 (contains a)
        Argument2

So far, all this is standard issue automatic dependency injection. If there’s a concept that’s confusing, you may want to review the series so far before asking questions in the comments or on Stack Overflow.

Reporting Command

Similar to our argument replacement article, we’ve prepared a simple program that reports on the above object hierarchy in real time using some of PHP’s reflection features. Open up the command class and find the execute method

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed Pulsestorm_TutorialVirtualType!");  
    //$this->showNestedPropertiesForObject();
}

Comment out the writeln line, and uncomment the call to showNestedPropertiesForObject

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed Pulsestorm_TutorialVirtualType!");  
    //$this->showNestedPropertiesForObject();
}

If you run the command with the above in place, you’ll see the following output.

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

We’re not going to go too in-depth into how this reporting works, but if you look at the showNestedPropertiesForObject method.

#File: app/code/Pulsestorm/TutorialVirtualType/Command/Testbed.php    
protected function showNestedPropertiesForObject()
{
    $object_manager = $this->getObjectManager();        
    $example         = $object_manager->create('Pulsestorm\TutorialVirtualType\Model\Example');
    $this->output("First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object");        
    $properties     = get_object_vars($example);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }  

    $this->output("Next, we're going to report on the Example object's one property (an Argument1 class)");  
    $properties     = get_object_vars($example->property_of_example_object);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }  

    $this->output("Finally, we'll report on an Argument1 object, instantiated separate from Example");          
    $argument1  = $object_manager->create('Pulsestorm\TutorialVirtualType\Model\Argument1');
    $properties = get_object_vars($argument1);        
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }          
}                     

You’ll see the program

  1. Instantiates a Pulsestorm\TutorialVirtualType\Model\Example object via the object manager, and then reports on it

  2. Fetches the Example object’s property_of_example_object property and reports on the object inside of it (a Pulsestorm\TutorialVirtualType\Model\Argument1 object)

  3. Finally, we use the object manager to directly instantiate a Pulsestorm\TutorialVirtualType\Model\Argument1 object, and report on it.

We’ll use this reporting in the next section to analyze how our dependency injection configuration changes the system’s behavior.

Creating a Virtual Type

The stage is set. We’re ready to create our virtual type. As we mentioned in our introduction, creating a virtual type is sort of like creating a sub-class for an existing class

<?php
class OurVirtualTypeName extends \Pulsestorm\TutorialVirtualType\Model\Argument1
{
}

Except, we’re not doing it in code. To create a virtual type in Magento 2, just add the following configuration to the module’s di.xml

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      
</config>

The <virtualType/> nodes live directly under the main <config/> node. They have two attributes (name and type). The name attribute defines the name of our virtual type — this should be a globally unique identifier, similar to a class name. The type attribute is the real PHP class our virtual type is based on.

That’s all there is to defining a virtual type. Of course — simply defining a virtual type will have no effect on system behavior. If you clear your cache and re-run the command, the output will be exactly the same.

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Using the Virtual Type

The purpose of a virtual type is to take the place of PHP classes in argument replacement. For example, without virtual types, if we wanted to change the argument injected into the Example class’s constructor, we’d add a <type/> configuration something like this.

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml    
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">Some\Other\Class</argument>
        </arguments>
    </type>        

</config>

Of course, if you tried running the command with the above configuration in place (after clearing your cache), you’d see an error like the following.

$ php bin/magento ps:tutorial-virtual-type

  [ReflectionException]                  
  Class Some\Other\Class does not exist  

That’s because the class we tried to inject (Some\Other\Class) is not defined. Let’s try changing our configuration to match the following

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml      
<config>
    <!-- ... -->
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
    </virtualType>      

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">ourVirtualTypeName</argument>
        </arguments>
    </type>        

</config>

Here we’ve replaced Some\Other\Class with ourVirtualTypeName. You might expect the above configuration to also cause an error, (since there’s no class named ourVirtualTypeName), except if you run our command with the above in place —

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2

— there’s no error! That’s because there is a “class” named ourVirtualTypeName. It’s just not a real PHP class — instead it’s our virtual type. At their most basic level, virtual types allow you to create what amounts to a class alias for a PHP class, and use that alias in your di.xml configuration.

That said — our output still looks the same. It turns out that it takes more than the simple creation and use of a virtual type to have a measurable impact on our system.

Changing Virtual Type Behavior

Earlier we said creating a virtual type was sort of like creating a sub-class of another class.

class OurVirtualTypeName extends \Pulsestorm\TutorialVirtualType\Model\Argument1
{
}

If we created a real sub-class of another class, we could change all sorts of things (method definitions, properties, constants, traits, etc) about that class.

With virtual types, the only behavior you can change in your virtual sub-class is which dependencies are injected. If that’s not clear, give the following configuration a try.

#File: app/code/Pulsestorm/TutorialVirtualType/etc/di.xml    
<config>
    <virtualType name="ourVirtualTypeName" type="Pulsestorm\TutorialVirtualType\Model\Argument1">  
        <arguments>
            <argument name="the_argument" xsi:type="object">Pulsestorm\TutorialVirtualType\Model\Argument3</argument>
        </arguments>
    </virtualType>

    <type name="Pulsestorm\TutorialVirtualType\Model\Example">
        <arguments>
            <argument name="the_object" xsi:type="object">ourVirtualTypeName</argument>
        </arguments>
    </type>  
</config>

This configuration is identical to our previous configuration with one exception — we’ve added argument sub-nodes to our virtual type. Same as they would under a <type/> node, the <arguments/>, <argument/> nodes under the <virtualType/> node replaces the argument with the name “the_argument” with a Pulsestorm\TutorialVirtualType\Model\Argument3 object. The argument nodes for a virtual type behave exactly as they would for a regular <type/> node — except they only effect the virtual type and not the original parent class.

If that was hard to follow (and it was), try clearing your cache and running the command again

$ php bin/magento ps:tutorial-virtual-type
First, we'll report on the Pulsestorm\TutorialVirtualType\Model\Example object
The Property $property_of_example_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument1

Next, we're going to report on the Example object's one property (an Argument1 class)
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument3

Finally, we'll report on an Argument1 object, instantiated separate from Example
The Property $property_of_argument1_object
  is an object
  created with the class: 
  Pulsestorm\TutorialVirtualType\Model\Argument2     

This is the main selling point of virtual types. You’ll notice that the property_of_argument1_object property is now an Argument3 object — but only when that parameter’s owner class (Argument1) is instantiated by dependency injection in the Example class. When we instantiate Argument1 by itself — Magento does not inject a dependency.

Worth It?

On one hand, virtual types allow us even more specificity in argument replacement. Whereas regular argument replacement lets us effectively change the behavior of a class dependency when it’s used in a specific class — virtual types allow us to effectively change the behavior of a dependency when it’s used in a specific class — and when that specific class is, itself, used in a specific class. In theory, this is great, and offers the potential for greater system stability.

However, for day-to-day Magento development, I’m not sure virtual types will be worth the confusion. While there’s lots of programmers out there who can keep track of those three level deep dependencies in their head, in my own limited interactions with virtual types, I’ve had a hard time keeping track of what configuration injects what dependency, while also keeping track of the problem at hand.

Beyond being, perhaps, an abstraction too far, there’s another sort of confusion virtual types introduce — consider this di.xml configuration from the core code.

#File: app/code/Magento/Catalog/etc/di.xml
<type name="Magento\Catalog\Model\Session">
    <arguments>
        <argument name="storage" xsi:type="object">Magento\Catalog\Model\Session\Storage</argument>
    </arguments>
</type>

This appears to be a straight forward configuration for automatic constructor dependency injection — Magento will replace the storage constructor argument in Magento\Catalog\Model\Session with the PHP class Magento\Catalog\Model\Session\Storage.

Except — there is no PHP class Magento\Catalog\Model\Session\Storage.

If you look in the same di.xml file, you’ll see the following.

#File: app/code/Magento/Catalog/etc/di.xml 
<virtualType name="Magento\Catalog\Model\Session\Storage" type="Magento\Framework\Session\Storage">
    <arguments>
        <argument name="namespace" xsi:type="string">catalog</argument>
    </arguments>
</virtualType>

It turns out the Magento 2 core team has created virtual types with names that look like real PHP class names. While this helps ensure the names are globally unique — it can create confusion for developers who aren’t aware Magento\Catalog\Model\Session\Storage is a virtual type — especially developers who are still learning the ins and outs of Magento’s object manager system and class autoloading.

All in all, while I can see why the team responsible for creating Magento 2 might find value in virtual types — I’m not sure they’re the best choice for developers creating Magento stores and extensions — especially when there’s a much more powerful, and controllable means for extending Magento system behavior in the plugin system. This plugin system will be our final stop in the Magento object manager tutorial.

However, before we can get there, there’s a few loose ends to tie up. Next week we’ll be covering instance vs. singleton objects with dependency injection, creating non-injectable instance objects using factories, as well as Magento 2’s proxy objects.

Originally published August 2, 2015

Magento 2 Object Manager Argument Replacement

In the last article of our series, we covered the “class preference” feature of Magento’s object manager. This week we’ll cover a similar, but more selective, class replacement system built into Magento 2. While this article should be accesible as a stand-alone tutorial for programmers familiar with modern object oriented concepts, if you’re having trouble try starting at the beginning.

Installing the Module

Like our other articles, we’ve prepared a module that sets up the basic commands and classes we’re going to use. You can find the module on GitHub. As of this writing, the installation procedure for a Magento module isn’t 100% clear, so we recommend installing these tutorial modules manually using the latest tagged release. If you need help installing a Magento module manually, the first article in this series contains detailed instructions for doing so.

You’ll know you have the module installed correctly when you get the following output when running the ps:tutorial-object-preference command.

$ php bin/magento  ps:tutorial-object-manager-arguments
Installed!

Assuming you can run the ps:tutorial-object-preference command, we’re ready to start.

Constructor Arguments

We’re going to need a little more exposition before we can get to the meat of this article. The exposition should also serve as a good review of some basic Magento 2 concepts.

With the command installed, open up its definition file and examine the execute method

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    $output->writeln("Installed!");          
    //$this->showPropertiesForObject();
}

You’ll notice the writeln command, along side a commented out call to the command’s showPropertiesForObject method. Let’s change the command to call the showPropertiesForObject command instead of outputting the Installed! text

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function execute(InputInterface $input, OutputInterface $output)
{
    $this->output = $output;
    //$output->writeln("Installed!");          
    $this->showPropertiesForObject();
}

If you run the command now, you’ll see the following output

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

The Property $object2
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument2

The Property $scaler1
  is a string
  with a value of: foo

The Property $scaler2
  is an integer
  with a value of: 0

The Property $scaler3
  is a boolean
  with a value of: false

The Property $thearray
  is an array
  with the elements:
  0=>foo

All this is simple enough, although probably a little confusing given our complete lack of context. Let’s take a look at the definition of showPropertiesForObject.

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
protected function showPropertiesForObject()
{
    $object_manager = $this->getObjectManager();        
    $object         = $object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');
    $properties     = get_object_vars($object);
    foreach($properties as $name=>$property)
    {
        $this->reportOnVariable($name, $property);       
    }
}

This method is pretty straight forward. We fetch an instance of the object manager and use it to instantiate an object from the Pulsestorm\TutorialObjectManagerArguments\Model\Example class.

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
$object_manager = $this->getObjectManager();        
$object         = $object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');

Then, using PHP’s built-in global get_object_vars function, we fetch an array of the Example object’s properties, and then for each of these we pass the property to the reportOnVariable method

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Command/Testbed.php
$properties     = get_object_vars($object);
foreach($properties as $name=>$property)
{
    $this->reportOnVariable($name, $property);       
}

This reportOnVariable method produces the output we saw above.

The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

For each variable passed, the method will output the variable’s type, and then class and/or value (depending on the type). The implementation of reportOnVariable is beyond the scope of this article, but feel free to poke around if you’re feeling exploratory.

Going back to our command’s output

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

The Property $object2
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument2

The Property $scaler1
  is a string
  with a value of: foo

The Property $scaler2
  is an integer
  with a value of: 0

The Property $scaler3
  is a boolean
  with a value of: false

The Property $thearray
  is an array
  with the elements:
  0=>foo 

We see our Example object has six properties. A quick look at its definition file should reveal this to be true

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php
<?php    
namespace Pulsestorm\TutorialObjectManagerArguments\Model;
class Example
{
    public $object1;
    public $object2;
    public $scaler1;
    public $scaler2;
    public $scaler3;
    public $thearray;

    public function __construct(
        ExampleArgument1 $object1,
        ExampleArgument2 $object2,
        $scaler1='foo',
        $scaler2=0,
        $scaler3=false,
        $thearray=['foo'])        
    {
        $this->object1 = $object1;
        $this->object2 = $object2;    

        $this->scaler1 = $scaler1;
        $this->scaler2 = $scaler2;
        $this->scaler3 = $scaler3;        
        $this->thearray   = $thearray;                
    }
}  

Here we’re dealing with a pretty standard issue Magento 2 class file. Six variables are included in the constructor. Magento 2’s automatic dependency injection automatically injects the two objects

Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1
Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1

and the remaining parameters are either plain old PHP scalers, or the final argument, which is a PHP array.

Regular arguments may coexist with PHP dependency injection — if you’re wondering why this would be of any use, hold on for a few more paragraphs.

Object Manager Reminder

With that lengthy exposition out of the way we’re almost ready to start talking about today’s feature: Argument Replacement. Argument replacement is another feature that, while labeled as part of “dependency injection”, is really (from another point of view), part of the underlying object manager system.

You’ll remember from previous tutorials that for most of your day-to-day Magento programming, you won’t instantiate an object directly with the object manager as we have

$object_manager->create('Pulsestorm\TutorialObjectManagerArguments\Model\Example');

Instead, you’d inject your object in the __construct method of whatever class file your’e working on

public function __constrcut(
    \Pulsestorm\TutorialObjectManagerArguments\Model\Example $example        
);

With the above in place Magento, behind the scenes, will use the object manager to create the Pulsestorm\TutorialObjectManagerArguments\Model\Example object. We’re using the object manager in these tutorials for simplicity — every feature we talk about is also available to objects created via dependency injection.

Argument Replacement

ENOUGH ALREADY! Let’s get to it. Argument replacement is a powerful feature that gives us full control, via configuration, of what the object Manager will inject in the __construct method.

Like a lot of features in Magento 2, argument replacement is best understood by example. Consider our Example class’s constructor

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php    
public function __construct(
    ExampleArgument1 $object1,
    ExampleArgument2 $object2,
    $scaler1='foo',
    $scaler2=0,
    $scaler3=false,
    $thearray=['foo'])        
{
    $this->object1 = $object1;
    $this->object2 = $object2;    

    $this->scaler1 = $scaler1;
    $this->scaler2 = $scaler2;
    $this->scaler3 = $scaler3;        
    $this->thearray   = $thearray;                
}

Let’s say we didn’t want $scaler1 to be equal to foo. The object manager and dependency injection block us from using normal constructor parameters, so up until now we’ve been stuck. This is what argument replacement lets us do. Add the following nodes to the di.xml file

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

We’ll get to what our new <type/> node does in a second, but first lets clear our 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 then re-run our ps:tutorial-object-manager-arguments command. You’ll recall this command shows us the values of our Example object’s properties.

$ php bin/magento ps:tutorial-object-manager-arguments
#... snipped ...
The Property $scaler1
  is a string
  with a value of: bar
#... snipped ...       

You should see the value of the $scaler property has changed from foo to bar. This is what argument replacement does — it allows end-user-programmers to change the value of any dependency injected argument.

How it Works

Let’s take a look at our di.xml configuration again

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config>       
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <!-- ... --->
    </type>
</config>

Here we’ve introduced a new-to-us second-level configuration node named <type/>. The node refers to the class whose arguments we’re trying to change, in our case that’s Pulsestorm\TutorialObjectManagerArguments\Model\Example. The type name refers not to native PHP types, but instead the idea that all the classes you define in your system/application form their own type system.

For Magento 1 developers, another way to think of types might be class aliases

Mage::getModel('catalog/product');

Back in Magento 1 these class aliases also formed a type system. While Magento 2 eschews class aliases, the object manager effectively turns each class name (or, with our new nomenclature, type name) into an alias.

So, the <type/> node lets us tell Magento 2 which class’s argument we want to target. The inner nodes let us tell Magento 2 what we want to do with the arguments

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<config>       
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

The outer <arguments/> node lets Magento know we’re dealing with arguments — there’s other <type/> sub-nodes we’ll cover in future articles. Each single <argument/> node (no S) lets us change the inject value of a single __construct argument.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    

<argument name="scaler1" xsi:type="string">bar</argument>                   

The name of an argument comes from its PHP variable name as a parameter. i.e., because the parameter is named $scaler1

#File: app/code/Pulsestorm/TutorialObjectManagerArguments/Model/Example.php
public function __construct(
    //...
    $scaler1='foo',
    //...
{

the <argument/> attribute name is set to scaler1. The xsi:type node lets us tell Magento what sort of value we want to replace the existing value with. With those attributes set — Magento will use the inner text value of <argument/> as the new value to inject, (in our case, that’s bar).

The first time I saw a non-object parameter in a Magento 2 class’s __construct method I didn’t understand why it was there. It seemed like the object manager and type hint dependency injection both would preclude this parameter from being anything other than its default value. Once I discovered the argument replacement feature though, they made a lot more sense.

Let’s dive a little deeper into argument replacement. There’s additional nuances you’ll need to get the most from this feature.

Replacing Object Arguments

Speaking of objects, what do you think would happen if we tried replacing one of the object parameters? Let’s give it a try! Add the following node to your di.xml configuration.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="object1" xsi:type="string">bar</argument>
        </arguments>
    </type>
</config>

This is very similar to our previous configuration. The only difference is we’ve targeted the parameter named object1. Let’s clear our cache and try running our command with the above in place.

$ php bin/magento ps:tutorial-object-manager-arguments

  [ErrorException]                  
  Illegal string offset 'instance'  

Whoops! An error. While the error is somewhat cryptic, this is correct system behavior. We just tried to replace the $object1 parameter with a string, but remember that all dependency injected arguments have type hints.

namespace Pulsestorm\TutorialObjectManagerArguments\Model;
public function __construct(
    ExampleArgument1 $object1,
    //...
)

It’s impossible to replace an injected object with a non-object. Fortunately, it is possible to replace an injected object with a different object! Give the following configuration a try

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="object1" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent</argument>
        </arguments>
    </type>

</config>

We’ve changed two things in the above configuration. First, the xsi:type argument is now set to object. This lets the object manager know it should treat the node’s text content as a class instead of a raw string. This leads us nicely into the other thing we’ve changed, which is to set the <argument/> node’s contents to Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent. This is the object we want to replace the original with.

Clear your cache, run your command, and you should see the following.

$ php bin/magento ps:tutorial-object-manager-arguments
The Property $object1
  is an object
  created with the class: 
  Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent

That is, Magento is now injecting a Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent class for the first argument.

Inserting Class Constants

Here’s another feature the xsi:type attribute enables. Give the following a try

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... snipped ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="scaler2" xsi:type="const">Magento\Integration\Model\Integration::SETUP_TYPE</argument>
        </arguments>
    </type>
</config>

Here we’ve used an xsi:type of const, and a node value of Magento\Integration\Model\Integration::SETUP_TYPE to replace the scaler2 argument. Run the reflection command with the above configuration and you’ll see the following

$ php bin/magento ps:tutorial-object-manager-arguments
//... snipped ...
The Property $scaler2
  is a string
  with a value of: setup_type
//... snipped ...

So — where did the setup_type value come from? The const value in xsi:type allows you to insert the value of a class constant. Magento interprets the node value (Magento\Integration\Model\Integration::SETUP_TYPE) as the class Magento\Integration\Model\Integration and the constant SETUP_TYPE

#File: app/code/Magento/Integration/Model/Integration.php
namespace Magento\Integration\Model;
//...
class Integration extends \Magento\Framework\Model\AbstractModel
{

    //...
        const SETUP_TYPE = 'setup_type';
    //...
}

Replacing Arrays

PHP arrays are a bit of a special case for argument replacement. Let’s try replacing our thearray parameter by adding the following configuration to di.xml

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- ... -->
    <type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
        <arguments>
            <argument name="thearray" xsi:type="array">
                <item name="0" xsi:type="string">science</item>
                <item name="baz" xsi:type="string">baz</item>
                <item name="bar" xsi:type="string">bar</item>
            </argument>
        </arguments>
    </type>
</config>    

You’ll notice that instead of a simple string inside of <argument/>, there’s a new set of <item/> nodes. If we run our command with the above configuration, we should see the following

$ php bin/magento ps:tutorial-object-manager-arguments -v
//...

The Property $thearray
  is an array
  with the elements: 
  0=>science
  baz=>baz
  bar=>bar

That is, we’ve replaced the default array with a multiple item, mixed key array. If we take a closer look at the <item/> tags

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">baz</item>

We see that Magento will interpret an items name (baz) as an array key, and it will interpret the tag contents (baz) as the value. One interesting thing here is the xsi:type tag. This works the same as in argument — which means you can create an array with scalars (above), objects

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">Some\Php\Class</item>

or even other, nested arrays!

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<item name="baz" xsi:type="string">
    <item name="0" xsi:type="string">one</item>
    <item name="1" xsi:type="string">two</item>        
</item>    

Another interesting feature of arrays is how Magento handles multiple modules trying to “replace” the same array. For objects, and strings, and other scalar xsi:types like number or boolean, Magento operates on a “last module in wins” principle.

With arrays, however, Magento will merge the <items/>. This means it’s possible to have multiple modules contributing items to an array. In fact, for the past few articles, we’ve been relying on this functionality!

If you take a look at this module’s di.xml file, you’ll see the following nodes.

<!-- File: app/code/Pulsestorm/TutorialObjectManagerArguments/etc/di.xml -->    
<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="testbedCommand" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Command\Testbed</item>
        </argument>
    </arguments>
</type>  

This is the same sort of argument replacement we’ve been doing in this article. That is, we’re replacing (or, since it’s an array, merging) the commands parameter in the core Magento\Framework\Console\CommandList class. We’re merging in an Pulsestorm\TutorialObjectManagerArguments\Command\Testbed object.

If we look at the class’s constructor

#File: lib/internal/Magento/Framework/Console/CommandList.php
namespace Magento\Framework\Console;
//...
class CommandList
{
    //...
    public function __construct(array $commands = [])
    {
        $this->commands = $commands;
    }
    //...
}

We see $commands is an array. In fact — this is an array that contains a list of commands for the bin/magento command. All Magento modules add commands this way. You should be familiar with the cache:clean command. This command’s class file is here

#File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php
<?php    //...
namespace Magento\Backend\Console\Command;

class CacheCleanCommand extends AbstractCacheTypeManageCommand
{
    //...
}

However, it’s this module’s di.xml file that makes it available to the command line framework.

<!-- #File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php -->
<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <!-- ... snip ... -->
            <item name="cacheCleanCommand" xsi:type="object">Magento\Backend\Console\Command\CacheCleanCommand</item>
            <!-- ... snip ... -->
        </argument>
    </arguments>
</type>

This is just one example of how the core Magento framework treats dependency injection and the object manager as first class citizens.

Best Practices for Type Safety

While Magento’s use of PHP’s native type hints help enforce type safety during argument replacement, it is (as of this writing) technically possible to replace a scaler argument with an object. The configuration for that would look something like this

<!-- #File: app/code/Magento/Backend/Console/Command/CacheCleanCommand.php -->
<type name="Pulsestorm\TutorialObjectManagerArguments\Model\Example">
    <arguments>
        <argument name="scaler1" xsi:type="object">Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent</argument>
    </arguments>
</type>

Although this configuration will run, it does lead to some odd results. If we run our reflection command with the above configuration in place

$ php bin/magento ps:tutorial-object-manager-arguments -v
The Property $scaler1
  is an array
  with the elements: 
  instance=>Pulsestorm\TutorialObjectManagerArguments\Model\SomethingCompletelyDifferent

We see that Magento has, for reasons that are unclear and likely an unintended side effect, replaced the argument with an array instead of an object, and that the array contains the name of the object.

While the more clever and resourceful among you might start thinking of ways to take advantage of this behavior, I’d advise against it. Even if it did work, replacing an argument PHP expects to use as a string, number, etc, with an object would likely have unforeseen consequences.

Also, while we’re here, here’s a list of all the valid (as of this writing) xsi:types

xsi:type="array"
xsi:type="string"
xsi:type="object"
xsi:type="boolean"
xsi:type="const"
xsi:type="number"
xsi:type="string"
xsi:type="init_parameter"
xsi:type="null"

Most of these are self explanatory. The only non-scaler type we didn’t discuss was init_parameter which, at this point, is just a de-facto alias for const.

Selective Rewrites

From the point of view of a Magento 1 developer, argument replacement offers a more selective version of the old class rewrite functionality. In our example above — if we needed to change the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1 — (or something like tutorialobjectmanager/exampleargument1 in Magento 1 class alias speak) the only way to do it was with a global rewrite that changed the behavior of the class in the entire system. Not taking adequate care to maintain the old functionality was created extension conflicts and system instability in Magento 1.

While it’s not fool proof, Magento 2’s argument replacement feature allows us to effectively change the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\ExampleArgument1, but only when this class is used in the Pulsestorm\TutorialObjectManagerArguments\Model\Example class. This means the rest of the system is still using the original class, and there’s zero chance our argument replacement will effect those systems.

Of course, you are still changing the behavior of Pulsestorm\TutorialObjectManagerArguments\Model\Example system-wide, so it’s not fool proof, but speaking for myself it’s a welcome addition to Magento’s functionality.

ALL That said, there are features beyond argument replacement that go even further in stabilizing Magento customization. Out next stop on the Magento object manager train will be the virtualType system.

Originally published July 29, 2015