1

I have recently been asked to help maintain a Magento module, and while I generally understand what DI is and why we want to use it, I was a bit confused by the 'automatic' side of things in Magento.

Take controllers for example, we use routes and the URL path to define which controllers will handle certain URL requests, however, this means that we don't seem to instantiate the class as such, so does Magento truly 'automatically' instantiate/inject all of the objects used by the constructor?

For example, it appears that I don't instantiate my MyRedirect class and pass in the specified objects - I configure that controller to respond to a certain URL, but what about constructor arguments?

2 Answers 2

2

This is a nice question. It goes to the magic core of magento.
Here are some words about how the DI and class instantiation works in Magento. (Almost) all magento classes are instantiated by Magento\Framework\ObjectManager\ObjectManager::create() (or get which calls create but in case you already instantiated a class it returns the same instance.... basically pseudo-singleton pattern).
going deeper into this method you end up in Magento\Framework\ObjectManager\Factory\Dynamic\Developer::create() (or Magento\Framework\ObjectManager\Factory\Dynamic\Production or Magento\Framework\ObjectManager\Factory\Dynamic\Compiled depending on some environment and compilation settings).
This method reads the constructor signature of your class using \ReflectionClass ($parameters = $this->definitions->getParameters($type).. you can go deeper into this method to see what actually happens).
If the class has no parameters, it just instantiates the class (return new $type()).
If it does have parameters it instantiates in the same way the classes on which your class depends. For your each of the dependencies it does the same thing and further for the dependencies of the dependencies until everything is instantiated. (or it crashed in case you get a circular dependency like Class A depends on B which depends on C which depends on A).

So your helper will get instantiated when the controller gets instantiated and all the dependencies of your helper get "magically" instantiated.

"But wait... sometimes my constructor depends on an interface. see my example in the question.... there is a LoggerInterface in there. you cannot instantiate interfaces."

Yes this is true. The process is a bit more complex. You can specify "preferences" for interfaces (and for other classes). A preference is basically a default class name for that specific interface. It's just like saying "when you need to instantiate an interface, instantiate this class instead".

In the case of the LoggerInterface, there is this specified in app/etc/di.xml

<preference for="Psr\Log\LoggerInterface" type="Magento\Framework\Logger\LoggerProxy" />

This means that by default, everytime a class depends on Psr\Log\LoggerInterface, the class Magento\Framework\Logger\LoggerProxy will be instantiated.
You can specify preferences in any di.xml file of your modules.
This is a nice way to be able to swap core classes for yours and extend the functionality.

"OK, this is nice and it makes sense, but not all the parameters are interfaces or classes, you cannot instantiate strings or arrays".

Right again. You can also use the di.xml of your modules to specify parameters for your classes that are not instantiated.

Let's say you have a class like this:

namespace My\Module;
class SomeClass
{
    public function __construct(
        \Some\OtherClass $otherClass,
        string $code,
        array $config
        string $label = 'label'
    )
}

The dependency on \Some\OtherClass can be resolved as explained above... but what about the other 3 parameters?

You can add this in your di.xml file

<type name="My\Module"><!-- name of your class -->
    <arguments>
        <argument name="code" xsi:type="string">my_code</argument><!-- name attribute should be name of your variable in the constructor, xsi:type should match the variable type (sort of) -->
        <argument name="config" xsi:type="array">
             <argument name="item1" xsi:type="string">value1</argument>
             <argument name="item2" xsi:type="string">value2</argument>
        </argument>
    </arguments>
</type>

(here is a list of all xsi:type values allowed and what do they mean: https://magento.stackexchange.com/a/103618/146)

The xml above will result in your class being instantiated with the my_code for $code and for config you get

[
   'item1' => 'value1',
   'iitem2' => 'value1'
]

"but what about $label?".
$label has a default value so you don't need to specify a value. the default will be used. But you can still provide a different value in a similar way as you do for your other arguments.

<type name="My\Module"><!-- name of your class -->
    <arguments>
        <argument name="code" xsi:type="string">my_code</argument>
        <argument name="config" xsi:type="array">
             <argument name="item1" xsi:type="string">value1</argument>
             <argument name="item2" xsi:type="string">value2</argument>
        </argument>
        <argument name="label" xsi:type="string">my_label</argument>
    </arguments>
</type>

"Good, good. It starts to make sense, but one more thing. For example, I have 2 classes that depend on MyHelper, but I don't want them to share the same instance of it. For one of my classes I want to provide a different instance of MyHelper with different parameters".

For this you have virtual types. A virtual type is just an instance of a class with specified dependencies instead of the default ones. It is similar to declaring services in Symfony if you are more familiar with it.

Let's assume your MyHelper class looks like this:

namespace Vendor\Module\Helper;
class MyHelper
{
    public function __construct(
        \SomeOther\Dependency $dependency,
        string $code = 'code',
        string $label = 'label'
    );
}

and your 2 classes that have a dependency on MyHelper look like this

namespace Vendor\Module;
class FirstClass
{
    public function __construct(
       \Vendor\Module\Helper\MyHelper $helper,
       ....
    )
}
namespace Vendor\Module;
class SecondClass
{
    public function __construct(
       \Vendor\Module\Helper\MyHelper $helper,
       ....
    )
}

for FirstClass you want the MyHelper instantiated with the default values. for SecondClass you need an instance of MyHelper with code = 'other_code' and label other_label.
For FirstClass you do nothing... it just works like I explained above.
For SecondClass, you first create a virtual type in the same di.xml file

<virtualType name="MyHelperWithOtherCode" type="Vendor\Module\Helper\MyHelper">
    <arguments>
        <argument name="code" xsi:type="string">other_code</argument>
        <argument name="label" xsi:type="string">other_label</argument>
    </arguments>
</virtualType>

the "name" of the virtual type can be anything, but it must be unique. the "type" of your virtual type should be the name of the class you want to instantiate with different parameters.

And now we tell magento, that SecondClass should use this virtual type as a dependency.
Same di.xml

<type name="Vendor\Module\SecondClass"><!-- name of your class -->
    <arguments>
        <argument name="helper" xsi:type="object">MyHelperWithOtherCode</argument><!-- name attribute should be name of your variable in the constructor-->
    </arguments>
</type>

That's all I have for not. I know it looks like a lot, but it will get easier after some time. Let me know if it makes sense.

1
  • Thank you. That was very informative and cleared things up a lot!
    – user111598
    Apr 19, 2023 at 8:14
0

well, if your class requires some custom logic regarding the constructor, you would use a Factory: https://developer.adobe.com/commerce/php/development/components/factories/

However, if your custom class has a constructor that requires specific arguments rather than relying on DI (as your class can also use DI when it's being instantiated) - you may want to reconsider how that is built (move to setter methods, for example).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.