How to use the DI Container correctly

Hello all,

first: I read the documentation any many stack overflow questions + I’ve searched in the forum for a long time and I really tried to understand how to use the DI container but I was not really able to fully gasp into it.

I use Yii2 actively every single day for 4 years now with Craft CMS. I don’t know if this is somehow important, but I wanted mention it because Craft CMS only relies on the service locator and does not use the DI.

Looking at the 2nd example in the docs below resolving-dependencies

$container = new Container;
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

I really don’t understand when/how dependencies are passed via $config variable and when they are passed via attribute.
For example 'yii\db\Connection' has no custom constructor so dns etc are passed via $config parameter and then set via Yii::configure but why/how does the UserFinder receive the Connection $db?

How can I use that actively?

I’ve made some tests, so you maybe can understand my confusion. There is no need to read this if you already see my problem.
I have a module and in this module, there are certain components defined via setComponents. Currently all those rely only on the service locator, so they are all full of Yii::app and I tried to change that in order to understand how DI could be used instead.

Judging from the example above my first thought was: maybe it is passed automatically like here

public function __construct(Connection $db, $config = [])
....
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);

To debug and test it this is my Fractals component,

class Fractals extends Component
{
    /**
     * The DB connection
     * 
     * @var Connection $db
     */
    public $db;

    /**
     * Fractals constructor.
     *
     * @param \yii\db\Connection|null $db       the dependency that should be injected 
     * @param null                               $foo      just a random variable for testing
     * @param array                            $config
     */
    public function __construct(Connection $db = null, $foo  = null, $config = [])
    {
        echo "<pre>";
        var_dump($db);
        var_dump($foo);
        var_dump(array_keys($config));
        echo "</pre>";
        die();

        parent::__construct($config);
    }
}

So I did this in my Module (Fractals is my component)

Yii::$container->set(Fractals::class, [
    'class' => Fractals::class
]);
$this->setComponents([
    // ...
    'fractals' => Fractals::class
]);
// later I grab those in my Controllers or other components via
Module::getInstance()->get('fractals');

The first result was

$db = null
$foo = null
$config = []

I thought maybe I have to include all the dependencies there, it’s not mentioned in the docs this way but maybe it helps. Thus I did

Yii::$container->set(Fractals::class, [
    'class' => Fractals::class,
    'db' => Yii::$container->get('db')
]);

The result was an exception Failed to instantiate component or class "db". Okay… so I tested it with

Yii::$container->set(Fractals::class, [
    'class' => Fractals::class,
    'db' => Yii::$app->get('db')
]);

and the result was

$db = null
$foo = null
$config = [
    'db' => // the correct DB connection
]

But that’s actually not what I thought, the example function in the docs was

public function __construct(Connection $db, $config = [])

So maybe I need to register the dependency first in th DI?

Yii::$container->set('db', Yii::$app->get('db'));
Yii::$container->set(Fractals::class, [
    'class' => Fractals::class,
]);

But that prints the same all empty state

$db = null
$foo = null
$config = []

and a mix of the two above

Yii::$container->set('db', Yii::$app->get('db'));
Yii::$container->set(Fractals::class, [
    'class' => Fractals::class,
    'db' => Yii::$container->get('db')
]);

only passes the parameter via $config array as well

$db = null
$foo = null
$config = [
    'db' => // the correct DB connection
]

Of course I could just keep that and do it that way, but I would like to understand why it does not work and how it is supposed to be used. I’ve spend hours and see all those examples all over the place but they do not work for me for whatever reason.

2 Likes

I guess I finally found the issue. When a parameter has a default value, for example

public function __construct(\craft\db\Connection $db = null, $foo  = null, $config = [])

it won’t be injected, but when it’s

public function __construct(\craft\db\Connection $db, $foo, $config = [])

it works. Is that noted somewhere? At least I was not able to find it. Whats the reason for that decision and are there any plans to remove it for Yii3. Especially when you use classes that can be used outside of Yii3 things can become quite complicated

Another question: What’s the correct way to receive for example the Request component via DI Container?

public function __construct($request, $config = [])
// Exception -> Missing required parameter "request" when instantiating "modules\myspa\services\Fractals"

public function __construct(\yii\base\Request $request, $config = [])
// Exception -> Can not instantiate yii\base\Request.

public function __construct(\yii\web\Request $request, $config = [])
// Always provides a web Request even when it's a console application

Just using Yii::$app->get('request') seems to be so much easier and much less complicated but I would understand it anyway.

Thank you in advance

Is that noted somewhere?

I’ve checked. It seems it isn’t :frowning: That could be considered a documentation issue.

I don’t remember the reason, DI container was implemented by @qiang around 2013.

In Yii 3 it works a bit differently. According to this document: https://github.com/yiisoft/injector/tree/master/docs/en

What’s the correct way to receive for example the Request component via DI Container?

Request isn’t a service so it doesn’t really suit DI container but you can do a trick like the following:

function () use ($app) {
 return $app->request;
}

@samdark Thank you very much for your answer. I know it wasn’t easy to understand my confusion and it was a huge wall of text.

Another thing: How can I register a singleton via alias?
When I do

$container->setSingleton('fractals',[
    'class' => Fractals::class
]);

and have two different constructors that use it like that

public function __construct(Fractals $fractals, $config = [])

Both receive a different instance of it and I can’t use them.
It only works when I do not use an alias

That would work only if you get it the same way, by alias.

Alright, I think I got it know.
Just for the sake of learning it (and in preparation for Yii3) I removed nearly all Yii::$app calls from my code and used DI instead and it worked.

Thank you.

1 Like