background

October 28, 2023

Could the real Magento Object Manager please stand up?

Yireo Blog Post

Magento backend development is all about using the Object Manager properly, via its XML configuration layer, via constructor argument injection and sometimes, only sometimes, directly. But there are various instances of the Object Manager around. Which one is the real one?

The contract

First of all, there is an interface Magento\Framework\ObjectManagerInterface which guarantees that the functions create(), get() and configure() exist. The get() method is what you would use, when you would inject a dependency into your class via the class constructor. The create() method is what you would use, when you are implementing a Magento factory pattern. And the configure() method is normally not being used. Normally. That's the twist in this blog.

The interface, however, is not something you will be using in your code. It is implemented by various classes, but there is no way for you to inject the interface in your constructor and get the real instance instead (like a preference). The interface is only used as a contract between the various Object Managers.

The real Object Manager

The Object Manager that most of us are using, when developing code in Magento, is the class Magento\Framework\App\ObjectManager. This is the one that is handling the constructor injection in your classes, this is the one you would use directly when dealing with factories, proxies, builders or tests.

The class extends actually from another one - Magento\Framework\ObjectManager\ObjectManager - but don't use that parent: The parent is not configured properly for use in Magento. For instance, you would like to have the Object Manager configured via all of the di.xml files in your Magento application. That's what the class Magento\Framework\App\ObjectManager is for.

It offers two additional methods - getInstance() and setInstance() - which guarantee that the same Object Manager is first configured and then used. In tests, you'll often see the getInstance() method popup (and god forbid, in PHTML templates of bad extension vendors). Likewise, the setInstance() is used in tests, so that multiple instances of the Object Manager could be moved around.

It is all about configuring the Object Manager with various rules. Which is where the configure() method kicks in. For instance, when booting the application, the configure() method is called with an instance of Magento\Framework\App\ObjectManager\ConfigLoader which brings in all of the DI configuration files.

Unit test mockery

For unit tests, there is also an Object Manager available, but it has nothing to do with the story above: The class Magento\Framework\TestFramework/Unit/Helper/ObjectManager doesn't implement the Magento\Framework\ObjectManagerInterface interface and there is no get(), create() or configure().

Instead, there is a getObject() method that allows you to instantiate a specific class with all of its dependencies mocked. If you have played around with PHPUnit unit tests in Magento, then you'll know that creating instances with mocked dependencies can be quite a cumbersome task. This Object Manager makes things a bit easier. And if you don't want a generic mock, but a customized one, you simply pass it on in the second argument array.

Other than this, there are also other methods like getConstructArguments() (which is also used by the getObject() method) so you can customize constructor arguments (which might be mocks) more easily, and a method getCollectionMock() to turn a data array into a collection of models (which can be mocks again).

All in all, this is handy. But - from my perspective - it has less to do with the original Object Manager behaviour of Magento and more with PHPUnit mocking.

And then integration tests

Last but not least, there is the Magento\TestFramework\ObjectManager class. It extends Magento\Framework\App\ObjectManager (which again extends Magento\Framework\ObjectManager\ObjectManager which again implements Magento\Framework\ObjectManagerInterface), which means there is a full blown Object Manager available, that could be configured via di.xml files and/or manually. Besides the parent methods getInstance(), setInstance() and configure(), this Object Manager adds some more methods to allow for easier integration testing.

For instance, there is a addSharedInstance() method which allows you to map a specified class name (or interface name) to a custom object, which could be a mock. Similarly, there are methods like clearCache() and removeSharedInstance() to manipulate the same shared instances as well: The variable $_sharedInstances is actually a property of the parent of the parent - Magento\Framework\ObjectManager\ObjectManager - and one of the key ingredients of having a Object Manager in the first place. It is close to the story of DI preferences, but not the same.

At first, this addSharedInstance() method seems just a shortcut which can be accomplished as well by creating a new Object Manager, configuring it with a new test instance with configure() and activating it with setInstance(). But don't use the original Object Manager in your integration tests! It is not properly configured, while Magento\TestFramework\ObjectManager is (during the test bootstrap).

Shared instances versus preferences

Whenever you treat a dependency as a singleton, for instance when injecting it via the constructor, it becomes a shared instance by default: When same object that is injected in one place, is the same instance as the object of the same type that is injected in another place. In the parent Object Manager, the variable $_sharedInstances is only (!) used by the get() method. Well, and it is used in Magento\TestFramework\ObjectManager to allow for existing shared instances to be swapped out with new objects, during integration tests.

Preferences are slightly different: They are added to Magento\Framework\ObjectManager\Config\Config (which is used by the Object Manager) as a more dynamic way to determine what kind of class needs to be loaded how. A shared instance entry in the Object Manager can only be a mapping between classname and object, while a preference can be a mapping between a classname and an interface as well. The target of the preference is determined in runtime to allow for an interface to be mapped to different objects, depending on the circumstances (like what kind of area the code is running in, adminhtml or frontend).

In the normal Magento application, you don't necessarily need to know about this mechanism too much: You simply inject a classname into your code and the Object Manager finds out (via configuration) whether it is shared and/or whether it is a preference for something else. But integration tests, you need to be aware of this mechanism a bit more. When you are asking the (testing version of the) Object Manager to give you an instance, by default the question of whether that instance is shared and/or a preference for something else, is dealt with by default ... unless you are starting to mess around with the shared instances.

Overriding shared instances

When you add a new shared instance (addSharedInstance), you can also just override an existing instance. And this effectively means that you can override the mechanism of preferences with something of your own. As I personally interpret it, you would stay away from overriding shared instances, when your integration tests also want to test out the very behaviour of preferences (and DI types and DI Virtual Types for that matter), making sure that the test application resembles the actual application as much as possible. In this case, integration tests are more similar to functional tests than they are to unit tests.

However, if you want to test out how your own class works when you are messing around with dependencies in various ways, you would swap out shared instances. In this case, integration tests are more similar to unit tests than they are to functional tests.

In the end

It is nice to know a bit more about Object Manager. However, besides its internal workings and its API of XML files, most of its customizability lies in the field of testing. If you are dealing with integration testing, you need to know about this. If you are just using the Object Manager in your production code, it is less important. But hey, we are all writing integration tests, aren't we?

Posted on October 28, 2023

About the author

Author Jisse Reitsma

Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.

Sponsor Yireo

Looking for a training in-house?

Let's get to it!

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.

Do not miss out on what we say

This will be the most interesting spam you have ever read

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.