The Object Manager and Why You Do Not Use It
Magento builds your objects for you: ask through the constructor, and leave the object manager itself alone.
Magento\Framework\ObjectManagerInterface::get($type) | ::create($type, array $arguments = [])Magento instantiates nearly every class in the system for you. The object manager is the component that does it: given a class name, it reads the constructor by reflection, builds each parameter first, then builds the class. Its interface is Magento\Framework\ObjectManagerInterface, which exposes get($type) for a shared instance, create($type, array $arguments = []) for a new one, and configure(array $configuration).
How you ask
You ask by listing dependencies as constructor parameters, typed against interfaces. Nothing else is required. A class inside an enabled module — a unit of code under app/code/Vendor/Name that Magento loads as one package — gets its dependencies resolved with no configuration written at all.
Why direct use is rejected
Calling the object manager yourself runs, and still fails review. The dependency stops appearing in the constructor, so nothing can see it: not a reader, not static analysis, not the compiled dependency map, and not a test that wants to substitute it. Adobe's object manager documentation allows four exceptions: static magic methods such as __sleep() and __wakeup(), keeping an existing constructor backward compatible, global scope such as the fixtures of integration tests, and classes whose own job is creating objects, such as factories and proxies. (The technical guidelines themselves mention the object manager only to ban it from unit tests, rule 11.2.2.)
Making it take effect
Configuration is cached. After adding a class or editing di.xml, run bin/magento cache:flush. In production mode Magento does not generate missing classes at runtime, so run bin/magento setup:di:compile as well.
Example
<?php
declare(strict_types=1);
// app/code/Vendor/Diagnostics/Service/VersionReporter.php
namespace Vendor\Diagnostics\Service;
use Magento\Framework\App\ProductMetadataInterface;
class VersionReporter
{
public function __construct(
private readonly ProductMetadataInterface $metadata
) {
}
public function describe(): string
{
return sprintf(
'%s (%s)',
$this->metadata->getVersion(),
$this->metadata->getEdition()
);
}
}
// Output on an Open Source 2.4.9 install:
// 2.4.9 (Community)When to use it
- Adding a service class to your module and having its dependencies supplied without writing any configuration.
- Reviewing a third-party module and spotting ObjectManager::getInstance() calls that hide what a class depends on.
- Explaining why a class resolves in developer mode and fails after a production deploy.
More examples
The form that gets rejected in review
This works at runtime, which is why it keeps being written. It is still wrong: the class now has a hidden dependency that cannot be replaced from outside, and the standard review checklist rejects it. Reach for this example when you need to show a colleague what to look for in inherited code.
<?php
declare(strict_types=1);
// app/code/Vendor/Diagnostics/Service/RejectedReporter.php
namespace Vendor\Diagnostics\Service;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ProductMetadataInterface;
class RejectedReporter
{
public function version(): string
{
// Rejected: the dependency is not in the constructor, so it is
// invisible to readers, to static analysis and to compilation.
$metadata = ObjectManager::getInstance()
->get(ProductMetadataInterface::class);
return $metadata->getVersion();
}
}Shorthand and longhand: injection is a get() call
The two forms are the same operation. Seeing that makes the rule easier to keep: you are not avoiding the object manager, you are letting it run where it can also record what you depend on. Useful when someone argues that injection is a limitation rather than the same thing written better.
<?php
declare(strict_types=1);
// app/code/Vendor/Diagnostics/Service/ReportRunner.php
namespace Vendor\Diagnostics\Service;
class ReportRunner
{
// Shorthand: you declare the need and Magento satisfies it.
public function __construct(
private readonly VersionReporter $reporter
) {
}
// Longhand, performed by the framework rather than by you:
//
// $reporter = $objectManager->get(VersionReporter::class);
//
// A dependency is resolved with get() while its type is shared,
// which is the default, so every class asking for VersionReporter
// in one request receives that same instance. create() builds a
// new one each time; the framework uses it only for a type marked
// shared="false", and it is what a generated factory calls. The
// only difference is who writes the call.
public function run(): string
{
return $this->reporter->describe();
}
}The one place core really does call it
A generated proxy stores only three scalar properties when serialized, then has to find the object manager again on unserialize. There is no constructor to inject into during __wakeup(), which is exactly the first documented exception. Read this as the boundary of the rule, not as permission to copy the call elsewhere.
// generated/code/Magento/Store/Model/StoreManagerInterface/Proxy.php
// Written by the code generator, not by hand.
public function __sleep()
{
return ['_subject', '_isShared', '_instanceName'];
}
public function __wakeup()
{
$this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance();
}
Discussion