- Magento 2 Object Manager
- Magento 2’s Automatic Dependency Injection
- Magento 2 Object Manager Preferences
- Magento 2 Object Manager Argument Replacement
- Magento 2 Object Manager Virtual Types
- Magento 2 Object Manager: Proxy Objects
- Magento 2 Object Manager: Instance Objects
- Magento 2 Object Manager Plugin System
It’s been a long and winding road, but the end is in sight! In this, the penultimate article in our object manager tutorial, we’re going to discuss working with instance and non-injectable objects in Magento 2.
This article assumes a basic familiarity with Magento 2 concepts, and may brush past something complicated. If you’re confused you may want to start from the beginning, or use the comment system below to point to your Magento Stack Exchange question.
Let’s get to it!
Sample Code
We’ve prepared a small sample Magento 2 module that we’ll reference and use in this article. The module’s on GitHub, and if you’re not familiar with manually installing Magento 2 modules the first article in this series has a good overview for installing a module via one of the tagged releases
To test that you’ve installed the module correctly, try running the following command
$ php bin/magento ps:tutorial-instance-objects
You've installed Pulsestorm_TutorialInstanceObjects!
If you see the You've installed Pulsestorm_TutorialInstanceObjects!
message, you’re all set.
Shared/Unshared, Singleton/Instance
Back in our first article, we introduced two object manager methods for instantiating objects
$object = $manager->create('Pulsestorm\TutorialInstanceObjects\Model\Example');
$object = $manager->get('Pulsestorm\TutorialInstanceObjects\Model\Example');
The create
method will instantiate a new object each time it’s called. The get
method will instantiate an object once, and then future calls to get
will return the same object. This behavior is similar to Magento 1’s getModel
vs. getSingleton
factories
Mage::getModel('group/class'); // ->create, or instance object
Mage::getSingleton('group/class'); // ->get, or instance object
What we didn’t cover was how the automatic constructor dependency injection system decides which method to use when it encounters a constructor parameter. Consider a constructor that looks like this.
//...
use Pulsestorm\TutorialInstanceObjects\Model\Example;
//...
public function __construct(Example $example)
{
//is $example created with `get` or `create`?
$this->example = $example?
}
We know the parameter will be a Pulsestorm\TutorialInstanceObjects\Model\Example
object, but we don’t know if it will be a new instance of a Pulsestorm\TutorialInstanceObjects\Model\Example
object, or the same Pulsestorm\TutorialInstanceObjects\Model\Example
object passed/injected into other constructors.
By default, all objects created via automatic constructor dependency injection are “singleton-ish” objects — i.e. they’re created via the object manager’s get
method.
If you want a new instance of an object, i.e. you want the object manager to use create
, you’ll need to add some additional <type/>
configuration to your module’s di.xml
file.
If we wanted the object manager to instantiate Pulsestorm\TutorialInstanceObjects\Model\Example
as an instance object every time, we’d add the following configuration to our di.xml
<!-- File: app/code/Pulsestorm/TutorialInstanceObjects/etc/di.xml -->
<config>
<!-- ... -->
<type name="Pulsestorm\TutorialInstanceObjects\Model\Example" shared="false">
<!-- ... arguments/argument tags here if you want to change injected arguments -->
</type>
</config>
This is the same <type/>
tag we saw back in our argument replacement tutorial. The name
attribute should be the name of the class whose behavior you want to change.
The new-to-us attribute is shared
. If shared
is set to false
, then Magento 2 will use the create
method to instantiate an object every time it encounters Pulsestorm\TutorialObjectManager1\Model\Example
as an automatically injected constructor argument. The shared
attribute has no effect on objects instantiated directly via PHP’s new
keyword, or the object manager’s two methods.
This attribute is named shared
due to an implementation detail in the object manager. When you use get
to instantiate an object, the object manager stores all the already instantiated objects in a _sharedInstances
array.
#File: lib/internal/Magento/Framework/ObjectManager/ObjectManager.php
public function get($type)
{
$type = ltrim($type, '\\');
$type = $this->_config->getPreference($type);
if (!isset($this->_sharedInstances[$type])) {
$this->_sharedInstances[$type] = $this->_factory->create($type);
}
return $this->_sharedInstances[$type];
}
When you configure a specific type
(i.e. a specific PHP class) with shared="false"
, you’re telling Magento 2 that you don’t want to use this _sharedInstances
array.
So, all you need to remember here is shared="true"
is the default, and you’ll get a singleton-ish/global object. If you change your type configuration to shared="false"
, the automatic constructor dependency injection system will start instantiating a new parameter every-time a programmer instantiates the attribute’s owner object.
Magento 2 Factories
While the shared
attribute is a useful bit of duct tape for those times you need an injected dependency to be a brand new instance object, it’s not an ideal solution for every (or even most) cases where you don’t want singletons.
One problem with shared
is the injected dependency is still dependent on its owner object being shared or un-shared. There’s lots of times where you just need a new instance of an object and might not be able to refactor your object relationships to accomplish that.
A perfect example of this are CRUD data objects, such as Magento’s CMS page objects or the catalog product objects. In Magento 1 you’d create a CMS page object like this
Mage::getModel('page/cms')->load($id);
In Magento 2, these sorts of objects are called “non-injectables”. Dependency injection is meant for objects that “do some thing”, or “provide some service”. These data objects, however, are meant to “identify this specific thing”. What we’re going to look at next is how to use these sorts of objects without automatic constructor dependency injection tying our hands.
First, there’s nothing stopping you from directly instantiating an object via PHP’s new
method
$product = new \Magento\Cms\Model\Page;
However, if you were to do this, your CMS Page object loses out on all of Magento’s object manager features.
Fortunately, the Magento 2 core developers haven’t abandoned us. In Magento 2, you instantiate these sorts of non-injectable objects via factory objects. Like so much in Magento 2, an example is worth 1,000 words.
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
public function __construct(
\Magento\Cms\Model\PageFactory $pageFactory =
)
{
$this->pageFactory = $pageFactory;
return parent::__construct();
}
//...
public function execute(InputInterface $input, OutputInterface $output)
{
$page = $this->pageFactory->create();
foreach($page->getCollection() as $item)
{
$output->writeln($item->getId() . '::' . $item->getTitle());
}
$page = $this->pageFactory->create()->load(1);
var_dump($page->getData());
}
Here’s what’s going on: In the __constructor
method, the command uses Magento’s automatic constructor dependency injection to create a Magento\Cms\Model\PageFactory
object, and then (per Magento convention) assigns that object to the pageFactory
property.
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
public function __construct(
\Magento\Cms\Model\PageFactory $pageFactory =
)
{
$this->pageFactory = $pageFactory;
}
Then, in execute
, we use this factory object to create a CMS page object (using the factory’s create
method)
$page = $this->pageFactory->create();
In Magento 1, the above is roughly equivalent to
$page = Mage::getModel('cms/page');
In Magento 1, we had a set of factory methods (getModel
, ‘helper’, ‘createBlock’). In Magento 2 — every non-injectable object has its own factory object.
You can find the factory for any model class by appending the text Factory
to that model class’s name. Above we wanted to instantiate Magento\Cms\Model\Page
objects, so the factory class was
Object we Want: Magento\Cms\Model\Page
Factory to Use: Magento\Cms\Model\PageFactory
Similarly, here’s the two classes for a product object
Object we Want: Magento\Catalog\Model\Product
Factory to Use: Magento\Catalog\Model\ProductFactory
Once we use a factory to instantiate a class, we have most (if not all) of our old Magento 1 CRUD methods available to us (load
, getData
, getCollection
, etc.)
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
$page = $this->pageFactory->create();
foreach($page->getCollection() as $item)
{
$this->output($item->getId() . '::' . $item->getTitle());
}
$page = $this->pageFactory->create()->load(1);
This may take a little getting used to, but compared to the error prone XML configuration needed to use Magento 1’s factory methods, this is already a big win.
Factory Definitions and Code Generation
There’s one last thing to cover about factory objects in Magento 2 that might answer a few questions in your head
- Ugh! I need to define a bunch of boiler plate factory code? Lame.
- Where can I see what a factory object looks like?
If we start with the second and more adult question, you may be in for a small surprise. Based on the factory’s full class name, (Magento\Cms\Model\PageFactory
), you might expect to find it at one of the following locations
app/code/Magento/Cms/Model/PageFactory.php
lib/internal/Magento/Cms/Model/PageFactory.php
However, neither of these files exist.
That’s because Magento 2 uses automatic code generation to create factory classes. You may remember this code generation from the proxy object article. If you’ve actually run the code above, you’ll find the PageFactory
class in the following location.
var/generation/Magento/Cms/Model/PageFactory.php
While the specifics are beyond the scope of this article, whenever
- PHP encounters a class name that ends in
Factory
- And the autoloader can’t load that class because there’s no definition file
Magento 2 will automatically create the factory.
If you’re curious how this happens this Stack Exchange answer showing the Magento/Framework/Code/Generator/Autoloader
kickoff point is a good place to start.
Factories for All
Factories aren’t just for Magento core code — they’ll work with any module class. The sample module we had you install includes a Pulsestorm\TutorialInstanceObjects\Model\Example
object. Let’s replace the __construct
method with one that adds a factory class for the Example
object.
//...
use Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory;
//...
class Testbed extends Command
{
protected $exampleFactory;
public function __construct(ExampleFactory $example)
{
$this->exampleFactory = $example;
return parent::__construct();
}
}
Then, we’ll use that factory in execute
.
protected function execute(InputInterface $input, OutputInterface $output)
{
$example = $this->exampleFactory->create();
$output->writeln(
"You just used a" . "\n\n "
get_class($this->exampleFactory) . "\n\n" .
"to create a \n\n " .
get_class($example) . "\n");
}
Run our command with the above execute
method in place, and you should see the following.
$ php bin/magento ps:tutorial-instance-objects
You just used a
Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory
to create a
Pulsestorm\TutorialInstanceObjects\Model\Example
As you can see, this code ran without issue, despite our never defining a Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory
class. You can find the factory definition in the generated code folder
#File: var/generation/Pulsestorm/TutorialInstanceObjects/Model/ExampleFactory.php
<?php
namespace Pulsestorm\TutorialInstanceObjects\Model;
/**
* Factory class for @see \Pulsestorm\TutorialInstanceObjects\Model\Example
*/
class ExampleFactory
{
protected $_objectManager = null;
protected $_instanceName = null;
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$instanceName = '\\Pulsestorm\\TutorialInstanceObjects\\Model\\Example'
)
{
$this->_objectManager = $objectManager;
$this->_instanceName = $instanceName;
}
public function create(array $data = array())
{
return $this->_objectManager->create($this->_instanceName, $data);
}
}
As for the specific implementation — right now a factory’s create
method accepts an array of parameters and users the object manager to create the object. However, future versions of Magento may change how these factories work. By having the framework generate these factories Magento 2 saves us the error prone busy work of coding up this boilerplate and the core team maintains control over how the factories work.
Wrap Up
With this article and its six predecessors complete, we have a pretty good understanding of Magento’s object system, how that system impacts the shape of the Magento code base, and (most importantly) the basic understanding that’s critical if we want to apply reason to Magento 2 code in the real world.
There is, however, one last thing we need to cover, and that’s the object plugin system. The plugin system is the true successor to Magento 1’s class rewrite system, and with a solid understanding of the object manager and automatic constructor dependency injection, we’re ready to tackle this next time in our final Object Manager tutorial.