container-interop / container-interop

Containers interoperability

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Delegate lookup feature: request for comments

moufmouf opened this issue · comments

Hi,

This issue is a follow-up to the 2 merge requests regarding the delegate lookup feature (#20 and #8).

I merged all PRs into a special branch that will be easier to work on:

https://github.com/container-interop/container-interop/tree/delegate-lookup

I split the delegate lookup document into a main document and a meta document.

We definitely need some feedback on both.

I hope the meta document will help people joining the discussion to understand what was the process so far (especially useful when we open the discussion on the FIG mailing list again). If you could look at it and let me know if you think it represents what was said so far, it would be great.

hello @moufmouf

First sentence of 4.1 section:

Containers implementing this feature can perform dependency lookups in other containers.

I think it should be clear, it's not wrong but clear, by adding "through composite container."
Resulting in :

Containers implementing this feature can perform dependency lookups in other containers, through composite container.

I can provide a pull request with small changes to the doc for discution and or cherry-picking, what you think?

Hi Nelson,

I'm trying to introduce the notion of composite container in chapter 4.2, but I understand it can be a bit late.
Please do not hesitate to provide many small PRs where you think it is needed.

Containers implementing this feature can perform dependency lookups in other containers, through composite container.

I don't see the need for a composite container. I think adding that will mean adding explanation about composite containers, and then this document is going out of scope.

I think we should be as simple and direct as possible.


Edit: removed my last comment because it was completely wrong sorry!

i still keep my idea here ..
simple and direct is the way to go @mnapoli .. so

Containers implementing this feature can perform dependency lookups in other containers.

the question is how?

through composite container.

I see it as a clarification only not any implicit reason behind the change.... :)

Explanation as composite containers is done in point 4.2 as @moufmouf mention it.

I have pushed some very minor changes to the branch (without PR to avoid useless noise) and I have reviewed everything, I'm +1 for merging now!

By the way in the branch we have for now:

## Standards

### Available

- [`ContainerInterface`](src/Interop/Container/ContainerInterface.php) …

### Proposed

- [*Delegate lookup feature*](docs/Delegate-lookup.md) …

Should we move Delegate lookup feature to Available? Once we merge, the feature will be considered standardized and we can tag a new 1.1.0 release.

Thanks Matthieu!

I just put the Delegate lookup feature to "Available" in the branch.

@njasm @jeremeamia @Ocramius : Any last comment/objection? I'm ready to merge! (and therefore, we will finally be ready to send the whole topic to the PHP-FIG :) )

Hi,

In Delegate lookup feature Meta Document
at the end of "2. Why Bother?"
shouldn't the text bellow the image be
"In the sample above, entry 1 in container 1 is referencing entry 3 in container 2."
instead of
"In the sample above, entry 1 in container 1 is referencing entry 4 in container 2."?

Fix the text or change the image?

I just had an alternate idea for a case where a lookup may fail:

interface FallbackFetchContainer
{
    /* @return mixed */
    public function getWithFallback($name, callable $failureFallback);
}

Nothing will be returned if a fetch operation fails: instead, the callback will be used. Ideas? Stupid? Am I overthinking this?

@Ocramius unless I'm misunderstanding at which point you want to use that interface, that doesn't address the same problem.

Here we want to override where dependencies of the object are fetched. The delegate container is only used for fetching dependencies of the instance being resolved. get('foo') should resolve foo in the current container. The delegate one is only used for dependencies of foo.

@drealecs I've done the change, thanks.

Here we want to override where dependencies of the object are fetched.

callback would be called even with a different service name. I'm just considering not giving up on the interface approach until I'm out of ideas (now I have this one).

class Container1 implements FallbackFetchContainer
{
    public function getWithFallback($name, callable $fail)
    {
        if ('theThing' === $name) {
            return new TheHorribleThing($this->getWithFallback('theDependency', $fail));
        }

        return $fail($name, $this);
    }
}
class Container2 implements FallbackFetchContainer
{
    public function getWithFallback($name, callable $fail)
    {
        if ('theDependency' === $name) {
            return new TheHorribleDependency();
        }

        return $fail($name, $this);
    }
}

Then use them together:

class ChainedContainer implements ContainerInterface
{
    public function __construct(array $containers)
    {
        $this->containers = $containers;
    }

    public function get($name)
    {
        $next = function ($service, $currentContainer) use (& $containers, & $next) {
            foreach ($containers as $key => $container) {
                // ... 
            }
        };

        $next($name, null);
    }
}

Again: just an idea :-) It is just a stack of containers to be used during fetch attempts

Only the delegate container should be used to fetch dependencies. Fallback is not good enough, we want to be able to override. So use the delegate by default.

I don't remember the reason for this right now though…

Also with your solution I'm not sure how it would go for the "vice-versa" in:

entries of container 1 should be able to reference entries of container 2 and vice-versa

I'm afraid it might not be possible or might end up in a recursive loop. But maybe I didn't pay enough to the example with debug_backtrace().

Also the fact that containers have to call getWithFallback() to get dependencies inside getWithFallback() looks very similar to what the delegate feature is today. Except it sounds less practical to me: in get(), containers should call get() to fetch dependencies. In getWithFallback(), containers should call getWithFallback() to get dependencies…

Hi @Ocramius,

Thanks for the feedback!
As @mnapoli noted, the main problem with the fallback approach is that it makes it hard to go vice versa + it prevents overiding some dependencies easily.

What we want to do is chaining containers.

Let's image we have 3 containers:

A => B => C

Those 3 containers have instances:

Container A Container B Container C
Entry A1 Entry B1 Entry C1
Entry A2 Entry B2 Entry C2
Entry A3 Entry B3 Entry C3

In the "fallback" approach, A will fallback on B, that will fallback on C.
Therefore,

  • an entry in A can have dependencies on entries in A, B or C,
  • an entry in B can have dependencies on entries in B or C,
  • an entry in C can have dependencies on entries in C only

This is a shame because it would be great if entries in C could also have dependencies in container A or B... Basically, whatever the container it is in, an entry should be able to have dependencies in any other container.

To do this, the fallback container needs to be the "ChainedContainer". Furthermore, if we want to allow container A to "override" an entry in container B or container C (this is a nice feature), we should look directly in the "ChainedContainer" instead of looking in the local container and in the ChainedContainer after. In this scenario, we are no more doing a "fallback". Instead, we are delegating the lookup of dependencies to the "ChainedContainer". This is why we used the "delegate-lookup" word to coin this feature.

With this in mind, I would rewrite your classes this way:

class Container1 implements DelegateFetchContainer
{
    public function getWithDelegate($name, callable $delegate)
    {
        if ('theThing' === $name) {
            return new TheHorribleThing($delegate('theDependency'));
        }

        throw new NotFoundException(...);
    }
}
class Container2 implements DelegateFetchContainer
{
    public function getWithFallback($name, callable $delegate)
    {
        if ('theDependency' === $name) {
            return new TheHorribleDependency();
        }

        throw new NotFoundException(...);
    }
}

And the call:

$delegateCallback = function($entry) use ($chainedContainer, $delegateCallback) {
    // Note: this does not work because I'm using $delegateCallback before declaring it!
    // Not sure how to go around this...
    $chainedContainer->getWithDelegate($entry, $delegateCallback);
}

$chainedContainer->getWithDelegate('theThing', $delegateCallback);

Note: I'm not sure I understand your last ChainedContainer class with the call to debug_backtrace. I'm sure there is a clever idea here but I did not understand what you are trying to do.

Now, I see 2 drawbacks to the approach with the DelegateFetchContainer interface.

  • First: as @mnapoli noted, container developers will have to write 2 very similar methods: get and getWithFallback
  • Second: I'm almost sure that the delegate lookup will always be performed on the ChainedContainer. So basically, whenever
    we call the getWithFallback method, the second parameter will always be a callable that will fetch dependencies on the ChainedContainer.
    I've been thinking this thoroughly and so far, I see no other valid use case.
    If the $delegate callable is always the same, it makes sense to store it in the class, so that usage is easier for users.
    Therefore, we would rewrite classes this way:
class Container1 implements DelegateFetchContainer
{
    private $delegate;

    public function setDelegate(callable $delegate) {
        $this->delegate = $delegate;
    }

    public function getWithDelegate($name)
    {
        if ('theThing' === $name) {
            return new TheHorribleThing($this->delegate('theDependency'));
        }

        throw new NotFoundException(...);
    }
}

At this point, we are back at the ParentAwareContainerInterface that was discussed in #8, that was coined to be a bad idea and was replaced by a simple "delegate lookup" standard. The final implementation will look like this:

// No more interface
class Container1
{
    private $delegate;

    public function __construct(callable $delegate) {
        $this->delegate = $delegate;
    }

    public function getWithDelegate($name)
    {
        if ('theThing' === $name) {
            return new TheHorribleThing($this->delegate('theDependency'));
        }

        throw new NotFoundException(...);
    }
}

It is very close to the delegate lookup feature. The only difference is we are using a callable instead of a ContainerInterface to perform the lookup.

@Ocramius: Again, I'm not sure I understood what you meant to do with the debug_backtrace call, so I'm not sure I understood completely your idea. I just tried to summarize the process we went through to come up with the "delegate lookup" idea. We also started with the fallback container idea and ended up to the current proposal through the process I tried to ouline above. Let me know if you think something is flawed or if you want to explain a bit more your idea.

The debug backtrace was just an attempt that I left in the example without noticing it (I'll remove it) :-)
I would need to write some tests around the feature to see if it would actually work.

As far as I know, the suggested RFC proposes that each container has its own fallbacks: that is indeed not what I presented in the example

Just a quick precision: I'm really trying hard not to use the word "fallback" because this is not what we do. The suggested RFC proposes that implementing containers should allow registering a delegate container (not a fallback!). The delegate container will be used to fetch dependencies of the requested entry instead of the container.

The suggested RFC does not propose that each container has its own fallback.
Actually it does not state anything about the way we expect the delegate lookup feature to be used.
In particular, it does not make any reference to the notion of "composite container" even if the "composite container" is a big part of the puzzle (we decided not to suggest any use case because it is a RFC but maybe it is not a wise choice)....

How "delegate containers" and "composite containers" play together is detailed in the meta document

In this meta document, we already highlighted the shortcomings of the fallback strategy and why we should not do it.

Do not hesitate to write some tests and let us know what you think.

hey all, the doc looks good to me 👍

Hi all,

Hope you are enjoying the holidays.
I'd like to make a status update on this issue if you don't mind.

I'd like to merge the "delegate lookup" branch as soon as possible (because this is what we are waiting to go public on PHP-FIG mailing list).

@Ocramius , I think you are the last person having issues with the current "delegate lookup" proposal. I know you want to try hard not to give up on the "interface" approach. I'm not sure what to do now with your proposals in this thread. Are you currently trying to work on some alternative approach? Shall we expect more proposals of you in the coming days? Basically, I'd like to know if you still want us to wait before I merge the "delegate lookup" branch, and if yes, when you plan to propose an alternative solution (maybe based on callbacks?). Note that we can also merge the current "delegate lookup" branch, go on the FIG mailing list, and then, discuss alternatives again on the FIG mailing-list (since I'm sure there is going to be a fierce debate there :) )

Anyway, I just want to get a go or a no-go for the merge. Let me know!

@moufmouf no, I'm not working nor having time to think about it right now. I think the meta-document that you want to merge is fine, I'd just prefer to have an interface-based solution (and I think the catch is using a callback).

It can be fleshed out also later on if required, so I'm 👍 on merging this.

Excellent! 👍
Branch delegate-lookup has been merged into master.

Thanks to all of you for sharing your thoughts and ideas, we are now finally ready to "Move to the fig" => #16 !

Awesome 👯!

Congrats @moufmouf for all that work!

Hi all!

Sorry if this is a wrong place to post. Please direct me to an appropriate thread, if so.

I, like many other people, strongly feel that there definitely should be an interface to formalize the delegate lookup feature. Of course, forcing the implementation to have a setter would be wrong. But why not have a getter instead?
Specifically to demonstrate how this could work, I have created a quick implementation of a DI container, together with my take on the delegate lookup feature: please find it here.

I would really like to see the delegate lookup standard backed by an interface. Please let me know what you think about my idea.

Hey @XedinUnknown,

I must admit I won't have too much time to look at this until the beginning of next week but it looks interesting, especially the fact that you can have "Unlimited level of nesting". At least, we never spoke about such an idea.

What you could do however is start a new issue with your idea, or simply post it on the PHP-FIG mailing list (since we are in the process of adopting PSR-11 and all changes will likely be ported by PSR-11 rather than by container-interop now). So far discussions about PSR-11 have never been too much about the delegate lookup feature and maybe we might need some exposal for this.

@moufmouf, thanks for the quick response.

For some reason I always have trouble reading the mailing list, particularly understanding what is where. Due to this, I would much prefer a GitHub issue: it's so much easier to read and understand.
Nevertheless, I am happy to create the new thread, if this is a better way to go; please find it here.

Looking forward to your feedback.