d21ebf928f8e321bc9011668f807c9a1e7410f02
[GitHub/WoltLab/WCF.git] /
1 Delegate lookup feature Meta Document
2 =====================================
3
4 1. Summary
5 ----------
6
7 This document describes the *delegate lookup feature*.
8 Containers are not required to implement this feature to respect the `ContainerInterface`.
9 However, containers implementing this feature will offer a greater lever of interoperability
10 with other containers, allowing multiple containers to share entries in the same application.
11 Implementation of this feature is therefore recommanded.
12
13 2. Why Bother?
14 --------------
15
16 The [`ContainerInterface`](../src/Interop/Container/ContainerInterface.php) ([meta doc](ContainerInterface.md))
17 standardizes how frameworks and libraries make use of a container to obtain objects and parameters.
18
19 By standardizing such a behavior, frameworks and libraries relying on the `ContainerInterface`
20 could work with any compatible container.
21 That would allow end users to choose their own container based on their own preferences.
22
23 The `ContainerInterface` is also enough if we want to have several containers side-by-side in the same
24 application. For instance, this is what the [CompositeContainer](https://github.com/jeremeamia/acclimate-container/blob/master/src/CompositeContainer.php)
25 class of [Acclimate](https://github.com/jeremeamia/acclimate-container) is designed for:
26
27 ![Side by side containers](images/side_by_side_containers.png)
28
29 However, an instance in container 1 cannot reference an instance in container 2.
30
31 It would be better if an instance of container 1 could reference an instance in container 2,
32 and the opposite should be true.
33
34 ![Interoperating containers](images/interoperating_containers.png)
35
36 In the sample above, entry 1 in container 1 is referencing entry 3 in container 2.
37
38 3. Scope
39 --------
40
41 ### 3.1 Goals
42
43 The goal of the *delegate lookup* feature is to allow several containers to share entries.
44
45 4. Approaches
46 -------------
47
48 ### 4.1 Chosen Approach
49
50 Containers implementing this feature can perform dependency lookups in other containers.
51
52 A container implementing this feature:
53
54 - must implement the `ContainerInterface`
55 - must provide a way to register a *delegate container* (using a constructor parameter, or a setter, or any
56 possible way). The *delegate container* must implement the `ContainerInterface`.
57
58 When a *delegate container* is configured on a container:
59
60 - Calls to the `get` method should only return an entry if the entry is part of the container.
61 If the entry is not part of the container, an exception should be thrown (as required in the `ContainerInterface`).
62 - Calls to the `has` method should only return *true* if the entry is part of the container.
63 If the entry is not part of the container, *false* should be returned.
64 - Finally, the important part: if the entry we are fetching has dependencies,
65 **instead** of perfoming the dependency lookup in the container, the lookup is performed on the *delegate container*.
66
67 Important! By default, the lookup should be performed on the delegate container **only**, not on the container itself.
68
69 It is however allowed for containers to provide exception cases for special entries, and a way to lookup into
70 the same container (or another container) instead of the delegate container.
71
72 ### 4.2 Typical usage
73
74 The *delegate container* will usually be a composite container. A composite container is a container that
75 contains several other containers. When performing a lookup on a composite container, the inner containers are
76 queried until one container returns an entry.
77 An inner container implementing the *delegate lookup feature* will return entries it contains, but if these
78 entries have dependencies, the dependencies lookup calls will be performed on the composite container, giving
79 a chance to all containers to answer.
80
81 Interestingly enough, the order in which containers are added in the composite container matters. Indeed,
82 the first containers to be added in the composite container can "override" the entries of containers with
83 lower priority.
84
85 ![Containers priority](images/priority.png)
86
87 In the example above, "container 2" contains a controller "myController" and the controller is referencing an
88 "entityManager" entry. "Container 1" contains also an entry named "entityManager".
89 Without the *delegate lookup* feature, when requesting the "myController" instance to container 2, it would take
90 in charge the instanciation of both entries.
91
92 However, using the *delegate lookup* feature, here is what happens when we ask the composite controller for the
93 "myController" instance:
94
95 - The composite controller asks container 1 if if contains the "myController" instance. The answer is no.
96 - The composite controller asks container 2 if if contains the "myController" instance. The answer is yes.
97 - The composite controller performs a `get` call on container 2 for the "myController" instance.
98 - Container 2 sees that "myController" has a dependency on "entityManager".
99 - Container 2 delegates the lookup of "entityManager" to the composite controller.
100 - The composite controller asks container 1 if if contains the "entityManager" instance. The answer is yes.
101 - The composite controller performs a `get` call on container 1 for the "entityManager" instance.
102
103 In the end, we get a controller instanciated by container 2 that references an entityManager instanciated
104 by container 1.
105
106 ### 4.3 Alternative: the fallback strategy
107
108 The first proposed approach we tried was to perform all the lookups in the "local" container,
109 and if a lookup fails in the container, to use the delegate container. In this scenario, the
110 delegate container is used in "fallback" mode.
111
112 This strategy has been described in @moufmouf blog post: http://mouf-php.com/container-interop-whats-next (solution 1).
113 It was also discussed [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-33570697) and
114 [here](https://github.com/container-interop/container-interop/pull/20#issuecomment-56599631).
115
116 Problems with this strategy:
117
118 - Heavy problem regarding infinite loops
119 - Unable to overload a container entry with the delegate container entry
120
121 ### 4.4 Alternative: force implementing an interface
122
123 The first proposed approach was to develop a `ParentAwareContainerInterface` interface.
124 It was proposed here: https://github.com/container-interop/container-interop/pull/8
125
126 The interface would have had the behaviour of the delegate lookup feature but would have forced the addition of
127 a `setParentContainter` method:
128
129 ```php
130 interface ParentAwareContainerInterface extends ReadableContainerInterface {
131 /**
132 * Sets the parent container associated to that container. This container will call
133 * the parent container to fetch dependencies.
134 *
135 * @param ContainerInterface $container
136 */
137 public function setParentContainer(ContainerInterface $container);
138 }
139 ```
140
141 The interface idea was first questioned by @Ocramius [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-51721777).
142 @Ocramius expressed the idea that an interface should not contain setters, otherwise, it is forcing implementation
143 details on the class implementing the interface.
144 Then @mnapoli made a proposal for a "convention" [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-51841079),
145 this idea was further discussed until all participants in the discussion agreed to remove the interface idea
146 and replace it with a "standard" feature.
147
148 **Pros:**
149
150 If we had had an interface, we could have delegated the registration of the delegate/composite container to the
151 the delegate/composite container itself.
152 For instance:
153
154 ```php
155 $containerA = new ContainerA();
156 $containerB = new ContainerB();
157
158 $compositeContainer = new CompositeContainer([$containerA, $containerB]);
159
160 // The call to 'setParentContainer' is delegated to the CompositeContainer
161 // It is not the responsibility of the user anymore.
162 class CompositeContainer {
163 ...
164
165 public function __construct($containers) {
166 foreach ($containers as $container) {
167 if ($container instanceof ParentAwareContainerInterface) {
168 $container->setParentContainer($this);
169 }
170 }
171 ...
172 }
173 }
174
175 ```
176
177 **Cons:**
178
179 Cons have been extensively discussed [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-51721777).
180 Basically, forcing a setter into an interface is a bad idea. Setters are similar to constructor arguments,
181 and it's a bad idea to standardize a constructor: how the delegate container is configured into a container is an implementation detail. This outweights the benefits of the interface.
182
183 ### 4.4 Alternative: no exception case for delegate lookups
184
185 Originally, the proposed wording for delegate lookup calls was:
186
187 > Important! The lookup MUST be performed on the delegate container **only**, not on the container itself.
188
189 This was later replaced by:
190
191 > Important! By default, the lookup SHOULD be performed on the delegate container **only**, not on the container itself.
192 >
193 > It is however allowed for containers to provide exception cases for special entries, and a way to lookup
194 > into the same container (or another container) instead of the delegate container.
195
196 Exception cases have been allowed to avoid breaking dependencies with some services that must be provided
197 by the container (on @njasm proposal). This was proposed here: https://github.com/container-interop/container-interop/pull/20#issuecomment-56597235
198
199 ### 4.5 Alternative: having one of the containers act as the composite container
200
201 In real-life scenarios, we usually have a big framework (Symfony 2, Zend Framework 2, etc...) and we want to
202 add another DI container to this container. Most of the time, the "big" framework will be responsible for
203 creating the controller's instances, using it's own DI container. Until *container-interop* is fully adopted,
204 the "big" framework will not be aware of the existence of a composite container that it should use instead
205 of its own container.
206
207 For this real-life use cases, @mnapoli and @moufmouf proposed to extend the "big" framework's DI container
208 to make it act as a composite container.
209
210 This has been discussed [here](https://github.com/container-interop/container-interop/pull/8#issuecomment-40367194)
211 and [here](http://mouf-php.com/container-interop-whats-next#solution4).
212
213 This was implemented in Symfony 2 using:
214
215 - [interop.symfony.di](https://github.com/thecodingmachine/interop.symfony.di/tree/v0.1.0)
216 - [framework interop](https://github.com/mnapoli/framework-interop/)
217
218 This was implemented in Silex using:
219
220 - [interop.silex.di](https://github.com/thecodingmachine/interop.silex.di)
221
222 Having a container act as the composite container is not part of the delegate lookup standard because it is
223 simply a temporary design pattern used to make existing frameworks that do not support yet ContainerInterop
224 play nice with other DI containers.
225
226
227 5. Implementations
228 ------------------
229
230 The following projects already implement the delegate lookup feature:
231
232 - [Mouf](http://mouf-php.com), through the [`setDelegateLookupContainer` method](https://github.com/thecodingmachine/mouf/blob/2.0/src/Mouf/MoufManager.php#L2120)
233 - [PHP-DI](http://php-di.org/), through the [`$wrapperContainer` parameter of the constructor](https://github.com/mnapoli/PHP-DI/blob/master/src/DI/Container.php#L72)
234 - [pimple-interop](https://github.com/moufmouf/pimple-interop), through the [`$container` parameter of the constructor](https://github.com/moufmouf/pimple-interop/blob/master/src/Interop/Container/Pimple/PimpleInterop.php#L62)
235
236 6. People
237 ---------
238
239 Are listed here all people that contributed in the discussions, by alphabetical order:
240
241 - [Alexandru Pătrănescu](https://github.com/drealecs)
242 - [Ben Peachey](https://github.com/potherca)
243 - [David Négrier](https://github.com/moufmouf)
244 - [Jeremy Lindblom](https://github.com/jeremeamia)
245 - [Marco Pivetta](https://github.com/Ocramius)
246 - [Matthieu Napoli](https://github.com/mnapoli)
247 - [Nelson J Morais](https://github.com/njasm)
248 - [Phil Sturgeon](https://github.com/philsturgeon)
249 - [Stephan Hochdörfer](https://github.com/shochdoerfer)
250
251 7. Relevant Links
252 -----------------
253
254 _**Note:** Order descending chronologically._
255
256 - [Pull request on the delegate lookup feature](https://github.com/container-interop/container-interop/pull/20)
257 - [Pull request on the interface idea](https://github.com/container-interop/container-interop/pull/8)
258 - [Original article exposing the delegate lookup idea along many others](http://mouf-php.com/container-interop-whats-next)
259