Categories


Archives


Recent Posts


Categories


Magento 2 Object Manager Argument Replacement

astorm

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

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

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 
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
Series Navigation<< Magento 2 Object Manager PreferencesMagento 2 Object Manager Virtual Types >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 29th July 2015