auraphp / Aura.Di

Dependency Injection System
MIT License
349 stars 63 forks source link

Does Aura.Di have an equiv. to Laravel's "contextual binding"? #160

Closed far-blue closed 5 years ago

far-blue commented 7 years ago

Laravel's container supports what they call "Contextual Binding" where you can specify different values for constructor parameter resolution on an instance depending on where the instance is being used.

I recently ran into a need for this when writing some RabbitMQ code. I had a class that handled publishing data to an exchange and I wanted to use 2 instances of this class in different places in my code but in each place I wanted to provide an 'exchange' object instance that was instantiated with a different exchange name string.

If there is no similar functionality already, I thought a possibly easy to use generic approach would be the ability to set named keys on a specified class, like we do for parameter defining, but which have the extra feature that they 'percolate down' until they are consumed.

For instance: class A needs an instance of class B and this class B then needs an instance of class C. The constructor for class C has a constructor parameter called exchangeName. You could defined class A as having a key of exchangeName with a value of foo and when an instance of A is created, the instance of class C receives the value of foo on it's constructor for the parameter exchangeName.

This way, if both class A and class D need instances of class B with different values of exchangeName for class C this is easy to setup.

jakejohns commented 7 years ago

From Docs:

If we want to override the default $di->params values for a specific lazy instance, we can pass a $params array as the second argument to lazyNew() to merge with the default values.

I'm finding it a little hard to follow the single letter meta variables, but here's an example of what I think you're saying:

<?php
// @codingStandardsIgnoreFile

// Default params
$di->params[A::class] = ['B' => $di->lazyNew(B::class)];
$di->params[B::class] = ['C' => $di->lazyNew(C::class)];
$di->params[C::class] = ['exchangeName' => 'foo'];

// Overridden for D::class
$c_for_d = $di->lazyNew(C::class, ['exchangeName' => 'bar']);
$b_for_d = $di->lazyNew(B::class, ['C' =>  $c_for_d]);
$di->params[D:class] = ['B' => $b_for_d];
far-blue commented 7 years ago

Hello :)

Yes, I'm aware of the overriding but it means having to explicitly define all the dependencies down a chain which can get somewhat cumbersome - esp. when there's no sensible default value.

We ended up doing something like:

<?php
$di->params[A::class] = [
  'B' => $di->lazyNew(
    B::class,
    [
      'C' => $di->lazyNew(
        C::class,
        ['exchangeName' => 'foo']
      )
    ]
  )
];

$di->params[D::class] = [
  'B' => $di->lazyNew(
    B::class,
    [
      'C' => $di->lazyNew(
        C::class,
        ['exchangeName' => 'bar']
      )
    ]
  )
];

I was just thinking something like this might be neater:

<?php
$di->percolatedParams[A::class] = ['exchangeName' => 'foo'];
$di->percolatedParams[D::class] = ['exchangeName' => 'bar'];
jakejohns commented 7 years ago

Personally, I would prefer the explicit definition.

I'm not even sure how you're suggesting this new "feature" would work. Any dependency of A that just happens to have a parameter named exchangeName would get the value 'foo'?? That seems totally nuts to me.

The hyper abstracted example makes it hard to understand the specifics and suggest another solution. Other alternatives I would think would be to use some kind of creation pattern, like a factory or something that could encapsulate this conditional logic. Or maybe the B that you pass to D could actually be X extends B or something like that.

Someone else should weigh in, but it sounds a little fishy to me.

far-blue commented 7 years ago

I can understand if you aren't keen on my suggestion of implementation but basically I'm suggesting the equiv. of this: https://laravel.com/docs/5.5/container#contextual-binding

pmjones commented 7 years ago

I had a class that handled publishing data to an exchange and I wanted to use 2 instances of this class in different places in my code but in each place I wanted to provide an 'exchange' object instance that was instantiated with a different exchange name string.

My first inclination is to create two different named services, with the different exchange strings.

Then inject the one named service in the one class, and the other named service in the other class.

EDIT:

On reading further, note that params are "percolated" -- that is, child class params are "inherited" from their parent class definitions. Perhaps that's what you're getting at?

far-blue commented 7 years ago

I'm not sure there's a difference in the resulting code based on whether a named service or an instance with different parameters is used, really. Named services are a little clearer and more self-documenting in a way but you still have this repeated chain of explicit dependencies. Obviously it's just as possible to have an N-level stack as it is the example 3-level stack.

I'm not saying this is an every-day occurrence but we've hit it twice in 2 apps as we've started transitioning to developing around a Aura.Di.

pmjones commented 7 years ago

I suppose I'd have to see the specific code in question; it's difficult for me to "see" the problem in the abstracted example above.

dlundgren commented 6 years ago

I came up with this after attempting to modify the Resolver. I haven't tested this when using the AutoResolver.

https://gist.github.com/dlundgren/5c2fd8670cf6334a1b56ea607739463b

far-blue commented 6 years ago

@dlundgren that looks great and exactly the kind of behaviour I was interested in :)

harikt commented 5 years ago

@frederikbosch you may want to consider this issue which contains a solution by @dlundgren for v4 before we go for alpha or beta release.

frederikbosch commented 5 years ago

@harikt Thought about this. The only problem I have is that the syntax and methodology is completely different compared to the rest of the library. I would rather see something like the example below. Then the user will remain calling $di rather than the $resolver directly which is at the moment hidden from the outside caller.

$di->params[D::class]['param'] = $di->lazyNew(B::class)->withContext(C::class, ['name' => 'second']);
harikt commented 5 years ago

@frederikbosch I agree with your points. Was mentioning, this issues needs to be addressed if possible.

dlundgren commented 5 years ago

The only reason I used the resolver was because it was the quickest way for such a feature to work in v3. I also didn't think it was the best way to do it, which is why I never submitted a PR.

frederikbosch commented 5 years ago

In PR #181 you can see my suggestion how to solve this.

frederikbosch commented 5 years ago

Fixed by #181, which is merged since today.