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?
First of all, there is an interface
Magento\Framework\ObjectManagerInterface which guarantees that the functions
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 -
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
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
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
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,
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?