Decoding Object Instantiation Magic In Magento2

In this article, we’ll see what’s going on behind the scenes in Magento2 when you ask Object Manager to instantiate a class. In particular, I’d like to see what will happen if I do something like:

$magentoObjectManager->create('\Magento\Catalog\Model\Category')

To do so, we need to prepare some kind of testing environment. Let’s do that first.

If you still don’t have your Magento2 environment, it’s easy to install and get up and running with xDebug.

Creating a Playground

I’m a big fan of creating an isolated script while learning Magento framework. I found a great thread on StackExchange that deals with this task. So create a file test.php under your Magento2 root and paste the following snippet:

<?php 
// File MAGE_ROOT/test.php
require __DIR__ . '/app/bootstrap.php';

class TestApp extends \Magento\Framework\App\Http implements \Magento\Framework\AppInterface
{   
    public function launch()
    {
        echo get_class($this->_objectManager->create('\Magento\Catalog\Model\Category'));
        return $this->_response;
    }

    public function catchException(\Magento\Framework\App\Bootstrap $bootstrap, \Exception $exception)
    {
        return false;
    }
}

$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
$app = $bootstrap->createApplication('TestApp');
$bootstrap->run($app);

I know this may be a little confusing, specifically because it looks more complicated than the good old Magento1 way. That’s why I provided a link to StackExchange – it’s very nice explained, so I encourage you to take a look.

Anyhow, for now I’d like to focus your attention on the first line of launch() method, where we’re asking ObjectManager to create \Magento\Catalog\Model\Category for us. What do you think is going to be returned?

Okay, when you run this script in the browser, the returned value will be:

Magento\Catalog\Model\Category\Interceptor

This might not be what you expected, so let’s start digging into this and try to decode the process.

Before we begin

You should know that instantiating an object through ObjectManager directly is not considered as a good idea. The code will work, but it is best practice to rely on constructor dependency injection. So this article is intended to serve as a good starting point in explaining how ObjectManager is instantiating classes in Magento2.

Let’s start!

Since our focus is on line

$this->_objectManager->create('\Magento\Catalog\Model\Category');

our first stop is file mage2/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php. Let’s see the implementation of create() method.

public function create($type, array $arguments = [])
{
    $type = ltrim($type, '\\');
    return $this->_factory->create($this->_config->getPreference($type), $arguments);
}

This method is pretty easy, but we can raise two questions:

  1. What does $this->_config->getPreference($type) do?
  2. What is $this->_factory?

Magento’s preferences

We can find the answer on the first question by checking what Magento\Framework\ObjectManager\Config\Config::getPreference($type) does.

Magento2 promotes SOLID principles. Inversion of control, as one of those principles, makes dependency injection more convenient. How to resolve a given class or interface is defined in modules’ etc/di.xml which manages resolving and injecting those objects throughout Magento2 application.

So if you take a look at mage2/app/etc/di.xml, which is sort of initial configuration, you’ll see how implementations are bound to interfaces. Values provided in this file and all configurations across modules are merged into one big configuration object.

1_preferences

Merged config object under debugger – part of _preferences property

In plain English, for the first item in _preferences, we’re telling the object manager that whenever someone asks it to instantiate Psr\Log\LoggerInterface, it should actually instantiate Magento\Framework\Logger\Monolog class.

Now, back to our example – because _preferences does not contain \Magento\Catalog\Model\Category, nothing will happen and $this->_factory->create call will be like this:

return $this->_factory->create('\Magento\Catalog\Model\Category', []);

Let’s pick into \Magento\Framework\ObjectManager\Factory\Dynamic\Developer, because $this->_factory is the object of that class. This method is a central piece of the whole tutorial and I will be returning to it a couple of times. So make sure to double review the method (it’s simplified a bit in the listing):

public function create($requestedType, array $arguments = [])
{        
    $type = $this->config->getInstanceType($requestedType);
    $parameters = $this->definitions->getParameters($type);
    if ($parameters == null) {
        return new $type();
    }
    ...
    try {
        $args = $this->_resolveArguments($requestedType, $parameters, $arguments);
    } catch (\Exception $e) {
        throw $e;
    }
    return $this->createObject($type, $args);
}

Virtual types – a brief look

We already talked about automatic code generation as a new concept in Magento2. At this point, Magento checks if a generated class for \Magento\Catalog\Model\Category is available or not. If it is, it will use it instead of requested \Magento\Catalog\Model\Category. So let’s quickly see how it’s working.

This check is done in \Magento\Framework\Interception\ObjectManager\Config\Developer class

public function getInstanceType($instanceName)
{
    $type = parent::getInstanceType($instanceName);
    if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($instanceName)) {
        return $type . '\\Interceptor';
    }
    return $type;
}

On the very top, there’s a call to parent class that deals with the task and it looks like this:

public function getInstanceType($instanceName)
{
    while (isset($this->_virtualTypes[$instanceName])) {
        $instanceName = $this->_virtualTypes[$instanceName];
    }
    return $instanceName;
}

This is where it checks for another spot in configuration, called virtual types. Now, to be completely honest – I still don’t know too much about this area of Magento2. In theory, they are intended to allow programmers to change what dependencies are injected in a particular class. There’s a nice introduction by Alan Storm on the topic, so you may have a read.

Anyhow, let’s see how it looks in memory at this point of execution:

2_virtualTypes

Merged config object under debugger – part of _virtualTypes property

Important conclusion for us is that, because there’s no virtual type defined for our requested class, parent::getInstanceType() returns type, which remains the same – \Magento\Catalog\Model\Category.

Should I use interceptor?

Next, if we go back to \Magento\Framework\Interception\ObjectManager\Config\Developer::getInstanceType(), in the second line Magento is checking if there’s a plugin for \Magento\Catalog\Model\Category:

public function hasPlugins($type)
{
    if (isset($this->_intercepted[$type])) {
        return $this->_intercepted[$type];
    }
    return $this->_inheritInterception($type);
}

This is nothing else, but another check in configuration object, which looks like this:

3_intercepted

Config object under debugger – part of _intercepted property

Pay attention to the number of elements in _intercepted property (910 items). These are pulled in from cache and not from XML configuration. So this time, it will return true, and that’s why and when Magento appends Interceptor to the original requested type name.

Inspecting constructor parameters

Let’s go back to \Magento\Framework\ObjectManager\Factory\Dynamic\DeveloperAt this phase, Magento knows what class it’s going to instantiate, but since it’s extensively using dependency injection framework, it will need to inspect all parameters, and that’s happening in this call:

$parameters = $this->definitions->getParameters($type);

So let’s review in getParameters() function:

public function getParameters($className)
{
    if (!array_key_exists($className, $this->_definitions)) {
        $this->_definitions[$className] = $this->_reader->getConstructor($className);
    }
    return $this->_definitions[$className];
}

In this small piece of code, many things are happening. The goal for this method is to return back all constructor parameters for a given class (in our case, \Magento\Catalog\Model\Category\Interceptor). We can see class internal caching mechanism – when Magento computes this for the first time, it internally stores it under $this->_definitions property. But on the first run, it needs to ask PHP reflection API to get this done. It’s done with this call: $this->_reader->getConstructor($className), in \Magento\Framework\Code\Reader\ClassReader.

public function getConstructor($className)
{
    $class = new \ReflectionClass($className);
    $result = null;
    $constructor = $class->getConstructor();
    if ($constructor) {
        $result = [];
        /** @var $parameter \ReflectionParameter */
        foreach ($constructor->getParameters() as $parameter) {
            try {
                $result[] = [
                    $parameter->getName(),
                    $parameter->getClass() !== null ? $parameter->getClass()->getName() : null,
                    !$parameter->isOptional(),
                    $parameter->isOptional()
                        ? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null)
                        : null,
                ];
            } catch (\ReflectionException $e) {
                $message = $e->getMessage();
                throw new \ReflectionException($message, 0, $e);
            }
        }
    }
    return $result;
}

Generate a class for me, please!

There’s one little problem though here. PHP’s reflection API will try to autoload \Magento\Catalog\Model\Category\Interceptor class using \Magento\Framework\Code\Generator\Autoloader:

public function load($className)
{
    if (!class_exists($className)) {
        return Generator::GENERATION_ERROR != $this->_generator->generateClass($className);
    }
    return true;
}

but it won’t be able to find it, since class does not exist. That’s why and when Magento’s generator system comes into place. Now, I won’t go into the process of generating new class, because it’s complex. We just need to know that a new file will be generated under mage2/var/generation/Magento/Catalog/Model/Category/Interceptor.php and that one will be in use from that point (until you delete /var folder of course).

Composer as class autoloader

So now that we have generated class in place, every time it is requested by PHP Reflection API, Composer will be responsible to autoload it using PSR-4 standard.

public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

You can check the method findFile() under the same class, to wrap your head around the process. As a bottom line, I would say that the generated file will be found and included by Composer. It’s worth mentioning that this interceptor extends \Magento\Catalog\Model\Category and implements \Magento\Framework\Interception\InterceptorInterface, so Composer will have to load all of them and their parent classes recursively. I mention this just to get you familiar with how significant role Composer has in Magento2, when it comes to class autoloading.

Now we can go back to getConstructor() function where Magento leverages Reflection feature of PHP to figure out what are parameters for this class.

At the end of this process, Magento will have an ordered list of constructor parameters for \Magento\Catalog\Model\Category\Interceptor class. Let’s expand a couple of them, to better see what is stored for every parameter:

5_parameters

Ordered list of constructor parameters for \Magento\Catalog\Model\Category\Interceptor class

Each parameter is an array with the following entries:

  • (string) parameter name
  • (string|null) parameter type
  • (bool) whether this param is required
  • (mixed) default value

Resolving arguments

Generally, in this step we can have two scenarios:

In our case as we have seen, Category Interceptor has 21 parameters in its constructor. All of them needs to be resolved – based on their type, Magento will know what class to autoload and instantiate. Let’s see how this looks in the codebase (slightly simplified):

protected function _resolveArguments($requestedType, array $parameters, array $arguments = [])
{
    $resolvedArguments = [];
    ...
    foreach ($parameters as $parameter) {
        list($paramName, $paramType, $paramRequired, $paramDefault) = $parameter;
        ...
        $this->resolveArgument($argument, $paramType, $paramDefault, $paramName, $requestedType);

        $resolvedArguments[] = $argument;
    }
    return $resolvedArguments;
}

So for every class, resolveArgument() function will be called, which asks ObjectManager to either get or create current instance, based on shared property (defined in di.xml files). As you might imagine – shared property determines whether an instance is supposed to be a singleton or not. In case it is a singleton, ObjectManager has (potentially) already instantiated it, and stored it internally – so it will just return that object.

protected function resolveArgument(&$argument, $paramType, $paramDefault, $paramName, $requestedType)
{
    if ($paramType && $argument !== $paramDefault && !is_object($argument)) {
        $argumentType = $argument['instance'];

        if (isset($argument['shared'])) {
            $isShared = $argument['shared'];
        } else {
            $isShared = $this->config->isShared($argumentType);
        }

        if ($isShared) {
            $argument = $this->objectManager->get($argumentType);
        } else {
            $argument = $this->objectManager->create($argumentType);
        }
    }
}

When the whole thing ends up, Magento will internally have all 21 parameters instantiated.

resolved_Args

An array of instantiated constructor parameters for \Magento\Catalog\Model\Category\Interceptor class

Let’s create an object, finally!

At this phase, Magento will have everything computed. The only thing that remains is to actually instantiate the object:

return $this->createObject($type, $args);

which will internally call one of the ugliest method in Magento2 codebase, createObject:

protected function createObject($type, $args)
{
    switch (count($args)) {
        case 1:
            return new $type($args[0]);
        case 2:
            return new $type($args[0], $args[1]);
        case 3:
            return new $type($args[0], $args[1], $args[2]);
        ...
        case 15:
            return new $type(
                $args[0],
                $args[1],
                ...
                $args[14]
            );
        default:
            $reflection = new \ReflectionClass($type);
            return $reflection->newInstanceArgs($args);
    }
}

Please note that this is a shrinked version here. If you’ve ever wondered why this method has so many cases in switch statement, the reason is performance – instantiation through new keyword is slightly faster than reflection and most classes have less than 15 arguments so this switch covers most cases. But not ours obviously. In our example, it will end up in default branch and instantiate Category object by using reflection.

And that’s it! Our long trip throughout the code ends up here.

Milan Stojanov is a certified Magento developer and Tuts+ author on Magento Fundamentals premium course. He likes to read and write about web development.