Categories


Archives


Recent Posts


Categories


Magento 2 Object Manager: Instance Objects

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!

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

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

Let’s get to it!

Sample Code

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

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

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

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

Shared/Unshared, Singleton/Instance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Magento 2 Factories

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In Magento 1, the above is roughly equivalent to

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

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

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

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

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

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

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

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

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

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

Factory Definitions and Code Generation

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

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

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

However, neither of these files exist.

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

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

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

Magento 2 will automatically create the factory.

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

Factories for All

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

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

Then, we’ll use that factory in execute.

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

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

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

    Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory

to create a 

    Pulsestorm\TutorialInstanceObjects\Model\Example

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

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

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

    protected $_instanceName = null;

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

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

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

Wrap Up

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

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

Originally published August 30, 2015
Series Navigation<< Magento 2 Object Manager: Proxy ObjectsMagento 2 Object Manager Plugin System >>