DI Reference Resolving - Proposing ResolveInterface

The Container implementation checks all passed values if they are of Instance (Yii2) or Reference (Yii 3) and resolves them by calling get() of the container using the id stored in the reference object.

In Yii2 Instance has a member function called get which in turn calls container->get(). Unfortunately the container implementation does not call $v->get($this), but $this->get($v->id). And in Yii 3 get() in Reference is completely gone.

Why would I prefer the first over the latter despite it would finally do the same?

Sometimes it might be necessary to use “complex” values as parameters. The container can resolve a single object reference, but not an array of references. This requires the object to know about the container in order to finally resolve the passed references.

To solve this problem I propose a ResolveInterface:

    interface ResolveInterface
    {
       /**
       * @param Container $container
       * @return mixed
        public get ($container)
    }

Reference should implement this interface and just do

return $container->get($this->id);

But using this interface other types of references could be implemented (even custom references):

    class RefrenceArray implements ResolveInterface
    {
       /* ...  */
        public function get($container)
        {
            $result = [];
            foreach ($this->arrayItems as $k => $v) {
                $result[$k] = ($v instanceof ResolveInterface) ? $v->get($container) : $v;
            }
            return $result;
        }
    }

In the dependency declaration it just requires:

       [
            /* ... */
            'thePropertyTakingAnArrayOfObjects' => RefrenceArray:of([
                   Refrerence::to('foo'),
                   Reference::to('bar')])
            /* ... */
       ]

If the container implementation just checked on $dependency instanceof ResolveInterface and called $dependency->get($this) we would get a comfortable way to resolve anything.

IMO this is important because DI obviously becomes more important in Yii 3 and one of the goals is to separate the actual classes from the container. Thus, as much as possible needs to be done by the container.
Of course such problems could be worked around by not using a declaration of dependency, but closures to create the actual instance. But then we lose the elegance of configuration.
With such a interface we could do anything - even funny things like generating a random value:

    RandValue implements ResolveInterface
    {
        public function get($container)
        {
              return mt_rand(999);   /* We do not care about the container - we just need the value */
        }
    }

I know that this is a silly example, but it might show the flexibility the approach provides.

Indeed, that adds flexibility. Need to think about it.

It is also comfortable way to create magic boxes with hidden dependencies, so you’re loosing one of the features of dependency injection. :slight_smile:

Well my major issue is that decoupling does not really work if not everything is resolved at the time (before) the object is created.

We often have this in the components now. The typical case:

$cache = ‘cache’;

With description “Can be a configuration array or id or object”.

Now it’s done in init() with Instance::ensure. But if we want a comfortable way to resolve this by DI it needs some “magic” when the object is created. This way the object does not need to be aware where stuff comes from.

Why would you need such magic? What is wrong with defining dependencies in constructor?

First of all DI also needs to work well with third party classes. And even in constructors it might be necessary to pass an array of objects

Example I came across lately:

A Multi-Level Cache implementation: The cache classes all implement a “CacheInterface”. But the actual MultiLevelCache expects an array of cache-objects it uses to lookup.

So basically there are two ways: Do it the “manual” way and use a callback in the DI in order to Create the object - or define the reference. If it’s just one object everything works fine. Then it’s just the reference object which already gets resolved. But an array of references is unknown to the DI implementation, thus does not get resolved.

And in other cases stuff needs to be set via setters (properties). Here the same problem shows up.

When it’s just about my own things I use in an application I have no problem with all this. I can even access the container from within the constructure and it’s OK for me.

All this is more about extension design (A extension developer cannot assume a specific structure of an application - esp. as it not longer recommended to use the traditional Yii::$app approach). Thus DI will need way more flexibility in order to be as comfortable to use as the traditional component approach.
The second thing is using classes which were not really designed for Yii. They might not be designed with DI in mind. They just expect values or objects in any way. If DI implementation does not provide the necessary flexibility we might end up having a lot of little pieces of callbacks where the objects are created.

I think one of the strengths of the older yiis is it’s easy of use. Extension components are often just “add it to the components array and set a few (if any) properties - and done”.
DI is a more powerful, but also more complicated approach. So I think it should be taken care that it can be used almost as comfortable as the traditional Components approach (have friendly defaults, configure by definition, etc), but with the added value that it does not just work for yii-specific classes.
Decoupling is (theoretically) one of the strengths of DI: Objects just get Objects passed and do not need to care where they come from. But this does not longer the case if the constructor needs to take care of resolving references or other things. And if they are not designed for Yii or containers in mind they might not even try it. They would just react with “Array of with Reference objects? What’s that? I want Foos there - nothing else”.

Maybe I’m missing something, but your examples looks like “how to deal with badly designed classes in convenient way”. But creating convenient way for dealing with such cases will promote bad design - you will get more badly designed classes because “Hey, Yii has dedicated feature for this, so this is fine to put more magic and hidden/unclear dependencies in my component”.

Id not think so. All this stuff works perfectly fine when stuff is initiated manually. It’s not necessarily bad design. And it’s also not about Yii. Many libraries are just independend of a specific framework, thus “assume” that it’s done manually.
Passing an array of objects to a constructor or a setter is not necessarily bad design. It’s just “Hey, dear object, here is the list of objects you have to deal with”. If the Object does not know about the concept of references and about the container it just cannot resolve them, thus they already need to be resolved.
If we design classes specifically for yii we can deal with the strengths and limitations. But I think DI - which becomes even more important with Yii 3 - should be able to deal with not just very basic stuff.