1 | <?php |
||||||
2 | /** |
||||||
3 | * @author Todd Burry <[email protected]> |
||||||
4 | * @copyright 2009-2017 Vanilla Forums Inc. |
||||||
5 | * @license MIT |
||||||
6 | */ |
||||||
7 | |||||||
8 | namespace Garden\Container; |
||||||
9 | |||||||
10 | use Psr\Container\ContainerInterface; |
||||||
11 | |||||||
12 | /** |
||||||
13 | * An inversion of control container. |
||||||
14 | */ |
||||||
15 | class Container implements ContainerInterface { |
||||||
16 | private $currentRule; |
||||||
17 | private $currentRuleName; |
||||||
18 | private $instances; |
||||||
19 | private $rules; |
||||||
20 | private $factories; |
||||||
21 | |||||||
22 | /** |
||||||
23 | * Construct a new instance of the {@link Container} class. |
||||||
24 | */ |
||||||
25 | 100 | public function __construct() { |
|||||
26 | 100 | $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => null]]; |
|||||
27 | 100 | $this->instances = []; |
|||||
28 | 100 | $this->factories = []; |
|||||
29 | |||||||
30 | 100 | $this->rule('*'); |
|||||
31 | 100 | } |
|||||
32 | |||||||
33 | /** |
||||||
34 | * Deep clone rules. |
||||||
35 | */ |
||||||
36 | 1 | public function __clone() { |
|||||
37 | 1 | $this->rules = $this->arrayClone($this->rules); |
|||||
38 | 1 | $this->rule($this->currentRuleName); |
|||||
39 | 1 | } |
|||||
40 | |||||||
41 | /** |
||||||
42 | * Clear all instances |
||||||
43 | * |
||||||
44 | */ |
||||||
45 | 1 | public function clearInstances() { |
|||||
46 | 1 | $this->instances = []; |
|||||
47 | 1 | } |
|||||
48 | |||||||
49 | /** |
||||||
50 | * Deep clone an array. |
||||||
51 | * |
||||||
52 | * @param array $array The array to clone. |
||||||
53 | * @return array Returns the cloned array. |
||||||
54 | * @see http://stackoverflow.com/a/17729234 |
||||||
55 | */ |
||||||
56 | 1 | private function arrayClone(array $array) { |
|||||
57 | return array_map(function ($element) { |
||||||
58 | 1 | return ((is_array($element)) |
|||||
59 | 1 | ? $this->arrayClone($element) |
|||||
60 | 1 | : ((is_object($element)) |
|||||
61 | ? clone $element |
||||||
62 | 1 | : $element |
|||||
63 | ) |
||||||
64 | ); |
||||||
65 | 1 | }, $array); |
|||||
66 | } |
||||||
67 | |||||||
68 | /** |
||||||
69 | * Normalize a container entry ID. |
||||||
70 | * |
||||||
71 | * @param string $id The ID to normalize. |
||||||
72 | * @return string Returns a normalized ID as a string. |
||||||
73 | */ |
||||||
74 | 101 | private function normalizeID($id) { |
|||||
0 ignored issues
–
show
|
|||||||
75 | 101 | return ltrim($id, '\\'); |
|||||
76 | } |
||||||
77 | |||||||
78 | /** |
||||||
79 | * Set the current rule to the default rule. |
||||||
80 | * |
||||||
81 | * @return $this |
||||||
82 | */ |
||||||
83 | 1 | public function defaultRule() { |
|||||
84 | 1 | return $this->rule('*'); |
|||||
85 | } |
||||||
86 | |||||||
87 | /** |
||||||
88 | * Set the current rule. |
||||||
89 | * |
||||||
90 | * @param string $id The ID of the rule. |
||||||
91 | * @return $this |
||||||
92 | */ |
||||||
93 | 100 | public function rule($id) { |
|||||
94 | 100 | $id = $this->normalizeID($id); |
|||||
95 | |||||||
96 | 100 | if (!isset($this->rules[$id])) { |
|||||
97 | 47 | $this->rules[$id] = []; |
|||||
98 | } |
||||||
99 | 100 | $this->currentRuleName = $id; |
|||||
100 | 100 | $this->currentRule = &$this->rules[$id]; |
|||||
101 | |||||||
102 | 100 | return $this; |
|||||
103 | } |
||||||
104 | |||||||
105 | /** |
||||||
106 | * Get the class name of the current rule. |
||||||
107 | * |
||||||
108 | * @return string Returns a class name. |
||||||
109 | */ |
||||||
110 | 2 | public function getClass() { |
|||||
111 | 2 | return empty($this->currentRule['class']) ? '' : $this->currentRule['class']; |
|||||
112 | } |
||||||
113 | |||||||
114 | /** |
||||||
115 | * Set the name of the class for the current rule. |
||||||
116 | * |
||||||
117 | * @param string $className A valid class name. |
||||||
118 | * @return $this |
||||||
119 | */ |
||||||
120 | 10 | public function setClass($className) { |
|||||
121 | 10 | $this->currentRule['class'] = $className; |
|||||
122 | 10 | return $this; |
|||||
123 | } |
||||||
124 | |||||||
125 | /** |
||||||
126 | * Get the rule that the current rule references. |
||||||
127 | * |
||||||
128 | * @return string Returns a reference name or an empty string if there is no reference. |
||||||
129 | */ |
||||||
130 | 3 | public function getAliasOf() { |
|||||
131 | 3 | return empty($this->currentRule['aliasOf']) ? '' : $this->currentRule['aliasOf']; |
|||||
132 | } |
||||||
133 | |||||||
134 | /** |
||||||
135 | * Set the rule that the current rule is an alias of. |
||||||
136 | * |
||||||
137 | * @param string $alias The name of an entry in the container to point to. |
||||||
138 | * @return $this |
||||||
139 | */ |
||||||
140 | 4 | public function setAliasOf($alias) { |
|||||
141 | 4 | $alias = $this->normalizeID($alias); |
|||||
142 | |||||||
143 | 4 | if ($alias === $this->currentRuleName) { |
|||||
144 | 1 | trigger_error("You cannot set alias '$alias' to itself.", E_USER_NOTICE); |
|||||
145 | } else { |
||||||
146 | 3 | $this->currentRule['aliasOf'] = $alias; |
|||||
147 | } |
||||||
148 | 4 | return $this; |
|||||
149 | } |
||||||
150 | |||||||
151 | /** |
||||||
152 | * Add an alias of the current rule. |
||||||
153 | * |
||||||
154 | * Setting an alias to the current rule means that getting an item with the alias' name will be like getting the item |
||||||
155 | * with the current rule. If the current rule is shared then the same shared instance will be returned. You can add |
||||||
156 | * multiple aliases by passing additional arguments to this method. |
||||||
157 | * |
||||||
158 | * If {@link Container::addAlias()} is called with an alias that is the same as the current rule then an **E_USER_NOTICE** |
||||||
159 | * level error is raised and the alias is not added. |
||||||
160 | * |
||||||
161 | * @param string ...$alias The alias to set. |
||||||
162 | * @return $this |
||||||
163 | * @since 1.4 Added the ability to pass multiple aliases. |
||||||
164 | */ |
||||||
165 | 8 | public function addAlias(...$alias) { |
|||||
166 | 8 | foreach ($alias as $name) { |
|||||
167 | 8 | $name = $this->normalizeID($name); |
|||||
168 | |||||||
169 | 8 | if ($name === $this->currentRuleName) { |
|||||
170 | 1 | trigger_error("Tried to set alias '$name' to self.", E_USER_NOTICE); |
|||||
171 | } else { |
||||||
172 | 7 | $this->rules[$name]['aliasOf'] = $this->currentRuleName; |
|||||
173 | } |
||||||
174 | } |
||||||
175 | 8 | return $this; |
|||||
176 | } |
||||||
177 | |||||||
178 | /** |
||||||
179 | * Remove an alias of the current rule. |
||||||
180 | * |
||||||
181 | * If {@link Container::removeAlias()} is called with an alias that references a different rule then an **E_USER_NOTICE** |
||||||
182 | * level error is raised, but the alias is still removed. |
||||||
183 | * |
||||||
184 | * @param string $alias The alias to remove. |
||||||
185 | * @return $this |
||||||
186 | */ |
||||||
187 | 2 | public function removeAlias($alias) { |
|||||
188 | 2 | $alias = $this->normalizeID($alias); |
|||||
189 | |||||||
190 | 2 | if (!empty($this->rules[$alias]['aliasOf']) && $this->rules[$alias]['aliasOf'] !== $this->currentRuleName) { |
|||||
191 | 1 | trigger_error("Alias '$alias' does not point to the current rule.", E_USER_NOTICE); |
|||||
192 | } |
||||||
193 | |||||||
194 | 2 | unset($this->rules[$alias]['aliasOf']); |
|||||
195 | 2 | return $this; |
|||||
196 | } |
||||||
197 | |||||||
198 | /** |
||||||
199 | * Get all of the aliases of the current rule. |
||||||
200 | * |
||||||
201 | * This method is intended to aid in debugging and should not be used in production as it walks the entire rule array. |
||||||
202 | * |
||||||
203 | * @return array Returns an array of strings representing aliases. |
||||||
204 | */ |
||||||
205 | 6 | public function getAliases() { |
|||||
206 | 6 | $result = []; |
|||||
207 | |||||||
208 | 6 | foreach ($this->rules as $name => $rule) { |
|||||
209 | 6 | if (!empty($rule['aliasOf']) && $rule['aliasOf'] === $this->currentRuleName) { |
|||||
210 | 4 | $result[] = $name; |
|||||
211 | } |
||||||
212 | } |
||||||
213 | |||||||
214 | 6 | return $result; |
|||||
215 | } |
||||||
216 | |||||||
217 | /** |
||||||
218 | * Get the factory callback for the current rule. |
||||||
219 | * |
||||||
220 | * @return callable|null Returns the rule's factory or **null** if it has none. |
||||||
221 | */ |
||||||
222 | 2 | public function getFactory() { |
|||||
223 | 2 | return isset($this->currentRule['factory']) ? $this->currentRule['factory'] : null; |
|||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * Set the factory that will be used to create the instance for the current rule. |
||||||
228 | * |
||||||
229 | * @param callable|null $factory This callback will be called to create the instance for the rule. |
||||||
230 | * @return $this |
||||||
231 | */ |
||||||
232 | 10 | public function setFactory(callable $factory = null) { |
|||||
233 | 10 | $this->currentRule['factory'] = $factory; |
|||||
234 | 10 | return $this; |
|||||
235 | } |
||||||
236 | |||||||
237 | /** |
||||||
238 | * Whether or not the current rule is shared. |
||||||
239 | * |
||||||
240 | * @return bool Returns **true** if the rule is shared or **false** otherwise. |
||||||
241 | */ |
||||||
242 | 2 | public function isShared() { |
|||||
243 | 2 | return !empty($this->currentRule['shared']); |
|||||
244 | } |
||||||
245 | |||||||
246 | /** |
||||||
247 | * Set whether or not the current rule is shared. |
||||||
248 | * |
||||||
249 | * @param bool $shared Whether or not the current rule is shared. |
||||||
250 | * @return $this |
||||||
251 | */ |
||||||
252 | 47 | public function setShared($shared) { |
|||||
253 | 47 | $this->currentRule['shared'] = $shared; |
|||||
254 | 47 | return $this; |
|||||
255 | } |
||||||
256 | |||||||
257 | /** |
||||||
258 | * Whether or not the current rule will inherit to subclasses. |
||||||
259 | * |
||||||
260 | * @return bool Returns **true** if the current rule inherits or **false** otherwise. |
||||||
261 | */ |
||||||
262 | 2 | public function getInherit() { |
|||||
263 | 2 | return !empty($this->currentRule['inherit']); |
|||||
264 | } |
||||||
265 | |||||||
266 | /** |
||||||
267 | * Set whether or not the current rule extends to subclasses. |
||||||
268 | * |
||||||
269 | * @param bool $inherit Pass **true** to have subclasses inherit this rule or **false** otherwise. |
||||||
270 | * @return $this |
||||||
271 | */ |
||||||
272 | 3 | public function setInherit($inherit) { |
|||||
273 | 3 | $this->currentRule['inherit'] = $inherit; |
|||||
274 | 3 | return $this; |
|||||
275 | } |
||||||
276 | |||||||
277 | /** |
||||||
278 | * Get the constructor arguments for the current rule. |
||||||
279 | * |
||||||
280 | * @return array Returns the constructor arguments for the current rule. |
||||||
281 | */ |
||||||
282 | 2 | public function getConstructorArgs() { |
|||||
283 | 2 | return empty($this->currentRule['constructorArgs']) ? [] : $this->currentRule['constructorArgs']; |
|||||
284 | } |
||||||
285 | |||||||
286 | /** |
||||||
287 | * Set the constructor arguments for the current rule. |
||||||
288 | * |
||||||
289 | * @param array $args An array of constructor arguments. |
||||||
290 | * @return $this |
||||||
291 | */ |
||||||
292 | 27 | public function setConstructorArgs(array $args) { |
|||||
293 | 27 | $this->currentRule['constructorArgs'] = $args; |
|||||
294 | 27 | return $this; |
|||||
295 | } |
||||||
296 | |||||||
297 | /** |
||||||
298 | * Set a specific shared instance into the container. |
||||||
299 | * |
||||||
300 | * When you set an instance into the container then it will always be returned by subsequent retrievals, even if a |
||||||
301 | * rule is configured that says that instances should not be shared. |
||||||
302 | * |
||||||
303 | * @param string $name The name of the container entry. |
||||||
304 | * @param mixed $instance This instance. |
||||||
305 | * @return $this |
||||||
306 | */ |
||||||
307 | 9 | public function setInstance($name, $instance) { |
|||||
308 | 9 | $this->instances[$this->normalizeID($name)] = $instance; |
|||||
309 | 9 | return $this; |
|||||
310 | } |
||||||
311 | |||||||
312 | /** |
||||||
313 | * Add a method call to a rule. |
||||||
314 | * |
||||||
315 | * @param string $method The name of the method to call. |
||||||
316 | * @param array $args The arguments to pass to the method. |
||||||
317 | * @return $this |
||||||
318 | */ |
||||||
319 | 12 | public function addCall($method, array $args = []) { |
|||||
320 | 12 | $this->currentRule['calls'][] = [$method, $args]; |
|||||
321 | |||||||
322 | 12 | return $this; |
|||||
323 | } |
||||||
324 | |||||||
325 | /** |
||||||
326 | * Finds an entry of the container by its identifier and returns it. |
||||||
327 | * |
||||||
328 | * @param string $id Identifier of the entry to look for. |
||||||
329 | * @param array $args Additional arguments to pass to the constructor. |
||||||
330 | * |
||||||
331 | * @throws NotFoundException No entry was found for this identifier. |
||||||
332 | * @throws ContainerException Error while retrieving the entry. |
||||||
333 | * |
||||||
334 | * @return mixed Entry. |
||||||
335 | */ |
||||||
336 | 81 | public function getArgs($id, array $args = []) { |
|||||
337 | 81 | $id = $this->normalizeID($id); |
|||||
338 | |||||||
339 | 81 | if (isset($this->instances[$id])) { |
|||||
340 | // A shared instance just gets returned. |
||||||
341 | 22 | return $this->instances[$id]; |
|||||
342 | } |
||||||
343 | |||||||
344 | 77 | if (isset($this->factories[$id])) { |
|||||
345 | // The factory for this object type is already there so call it to create the instance. |
||||||
346 | 6 | return $this->factories[$id]($args); |
|||||
347 | } |
||||||
348 | |||||||
349 | 77 | if (!empty($this->rules[$id]['aliasOf'])) { |
|||||
350 | // This rule references another rule. |
||||||
351 | 3 | return $this->getArgs($this->rules[$id]['aliasOf'], $args); |
|||||
352 | } |
||||||
353 | |||||||
354 | // The factory or instance isn't registered so do that now. |
||||||
355 | // This call also caches the instance or factory fo faster access next time. |
||||||
356 | 77 | return $this->createInstance($id, $args); |
|||||
357 | } |
||||||
358 | |||||||
359 | /** |
||||||
360 | * Make a rule based on an ID. |
||||||
361 | * |
||||||
362 | * @param string $nid A normalized ID. |
||||||
363 | * @return array Returns an array representing a rule. |
||||||
364 | */ |
||||||
365 | 77 | private function makeRule($nid) { |
|||||
366 | 77 | $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : []; |
|||||
367 | |||||||
368 | 77 | if (class_exists($nid)) { |
|||||
369 | 68 | for ($class = get_parent_class($nid); !empty($class); $class = get_parent_class($class)) { |
|||||
370 | // Don't add the rule if it doesn't say to inherit. |
||||||
371 | 6 | if (!isset($this->rules[$class]) || (isset($this->rules[$class]['inherit']) && !$this->rules[$class]['inherit'])) { |
|||||
372 | 4 | continue; |
|||||
373 | } |
||||||
374 | 2 | $rule += $this->rules[$class]; |
|||||
375 | } |
||||||
376 | |||||||
377 | // Add the default rule. |
||||||
378 | 68 | if (!empty($this->rules['*']['inherit'])) { |
|||||
379 | 68 | $rule += $this->rules['*']; |
|||||
380 | } |
||||||
381 | |||||||
382 | // Add interface calls to the rule. |
||||||
383 | 68 | $interfaces = class_implements($nid); |
|||||
384 | 68 | foreach ($interfaces as $interface) { |
|||||
385 | 44 | if (isset($this->rules[$interface])) { |
|||||
386 | 10 | $interfaceRule = $this->rules[$interface]; |
|||||
387 | |||||||
388 | 10 | if (isset($interfaceRule['inherit']) && $interfaceRule['inherit'] === false) { |
|||||
389 | 1 | continue; |
|||||
390 | } |
||||||
391 | |||||||
392 | 9 | if (!isset($rule['shared']) && isset($interfaceRule['shared'])) { |
|||||
393 | 3 | $rule['shared'] = $interfaceRule['shared']; |
|||||
394 | } |
||||||
395 | |||||||
396 | 9 | if (!isset($rule['constructorArgs']) && isset($interfaceRule['constructorArgs'])) { |
|||||
397 | 3 | $rule['constructorArgs'] = $interfaceRule['constructorArgs']; |
|||||
398 | } |
||||||
399 | |||||||
400 | 9 | if (!empty($interfaceRule['calls'])) { |
|||||
401 | 2 | $rule['calls'] = array_merge( |
|||||
402 | 2 | isset($rule['calls']) ? $rule['calls'] : [], |
|||||
403 | 2 | $interfaceRule['calls'] |
|||||
404 | ); |
||||||
405 | } |
||||||
406 | } |
||||||
407 | } |
||||||
408 | 13 | } elseif (!empty($this->rules['*']['inherit'])) { |
|||||
409 | // Add the default rule. |
||||||
410 | 13 | $rule += $this->rules['*']; |
|||||
411 | } |
||||||
412 | |||||||
413 | 77 | return $rule; |
|||||
414 | } |
||||||
415 | |||||||
416 | /** |
||||||
417 | * Make a function that creates objects from a rule. |
||||||
418 | * |
||||||
419 | * @param string $nid The normalized ID of the container item. |
||||||
420 | * @param array $rule The resolved rule for the ID. |
||||||
421 | * @return \Closure Returns a function that when called will create a new instance of the class. |
||||||
422 | * @throws NotFoundException No entry was found for this identifier. |
||||||
423 | */ |
||||||
424 | 48 | private function makeFactory($nid, array $rule) { |
|||||
425 | 48 | $className = empty($rule['class']) ? $nid : $rule['class']; |
|||||
426 | |||||||
427 | 48 | if (!empty($rule['factory'])) { |
|||||
428 | // The instance is created with a user-supplied factory function. |
||||||
429 | 6 | $callback = $rule['factory']; |
|||||
430 | 6 | $function = $this->reflectCallback($callback); |
|||||
431 | |||||||
432 | 6 | if ($function->getNumberOfParameters() > 0) { |
|||||
433 | 3 | $callbackArgs = $this->makeDefaultArgs($function, (array)$rule['constructorArgs']); |
|||||
434 | $factory = function ($args) use ($callback, $callbackArgs) { |
||||||
435 | 3 | return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args)); |
|||||
436 | 3 | }; |
|||||
437 | } else { |
||||||
438 | 3 | $factory = $callback; |
|||||
439 | } |
||||||
440 | |||||||
441 | // If a class is specified then still reflect on it so that calls can be made against it. |
||||||
442 | 6 | if (class_exists($className)) { |
|||||
443 | 6 | $class = new \ReflectionClass($className); |
|||||
444 | } |
||||||
445 | } else { |
||||||
446 | // The instance is created by newing up a class. |
||||||
447 | 42 | if (!class_exists($className)) { |
|||||
448 | 1 | throw new NotFoundException("Class $className does not exist.", 404); |
|||||
449 | } |
||||||
450 | 41 | $class = new \ReflectionClass($className); |
|||||
451 | 41 | $constructor = $class->getConstructor(); |
|||||
452 | |||||||
453 | 41 | if ($constructor && $constructor->getNumberOfParameters() > 0) { |
|||||
454 | 38 | $constructorArgs = $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs']); |
|||||
455 | |||||||
456 | $factory = function ($args) use ($className, $constructorArgs) { |
||||||
457 | 37 | return new $className(...array_values($this->resolveArgs($constructorArgs, $args))); |
|||||
458 | 37 | }; |
|||||
459 | } else { |
||||||
460 | $factory = function () use ($className) { |
||||||
461 | 4 | return new $className; |
|||||
462 | 4 | }; |
|||||
463 | } |
||||||
464 | } |
||||||
465 | |||||||
466 | // Add calls to the factory. |
||||||
467 | 46 | if (isset($class) && !empty($rule['calls'])) { |
|||||
468 | 6 | $calls = []; |
|||||
469 | |||||||
470 | // Generate the calls array. |
||||||
471 | 6 | foreach ($rule['calls'] as $call) { |
|||||
472 | 6 | [$methodName, $args] = $call; |
|||||
473 | 6 | $method = $class->getMethod($methodName); |
|||||
474 | 6 | $calls[] = [$methodName, $this->makeDefaultArgs($method, $args)]; |
|||||
475 | } |
||||||
476 | |||||||
477 | // Wrap the factory in one that makes the calls. |
||||||
478 | $factory = function ($args) use ($factory, $calls) { |
||||||
479 | 6 | $instance = $factory($args); |
|||||
480 | |||||||
481 | 6 | foreach ($calls as $call) { |
|||||
482 | 6 | [$methodName, $defaultArgs] = $call; |
|||||
483 | 6 | $finalArgs = $this->resolveArgs($defaultArgs, [], $instance); |
|||||
484 | 6 | call_user_func_array( |
|||||
485 | 6 | [$instance, $methodName], |
|||||
486 | 6 | $finalArgs |
|||||
487 | ); |
||||||
488 | } |
||||||
489 | |||||||
490 | 6 | return $instance; |
|||||
491 | 6 | }; |
|||||
492 | } |
||||||
493 | |||||||
494 | 46 | return $factory; |
|||||
495 | } |
||||||
496 | |||||||
497 | /** |
||||||
498 | * Create a shared instance of a class from a rule. |
||||||
499 | * |
||||||
500 | * This method has the side effect of adding the new instance to the internal instances array of this object. |
||||||
501 | * |
||||||
502 | * @param string $nid The normalized ID of the container item. |
||||||
503 | * @param array $rule The resolved rule for the ID. |
||||||
504 | * @param array $args Additional arguments passed during creation. |
||||||
505 | * @return object Returns the the new instance. |
||||||
506 | * @throws NotFoundException Throws an exception if the class does not exist. |
||||||
507 | */ |
||||||
508 | 31 | private function createSharedInstance($nid, array $rule, array $args) { |
|||||
509 | 31 | if (!empty($rule['factory'])) { |
|||||
510 | // The instance is created with a user-supplied factory function. |
||||||
511 | 3 | $callback = $rule['factory']; |
|||||
512 | 3 | $function = $this->reflectCallback($callback); |
|||||
513 | |||||||
514 | 3 | if ($function->getNumberOfParameters() > 0) { |
|||||
515 | 1 | $callbackArgs = $this->resolveArgs( |
|||||
516 | 1 | $this->makeDefaultArgs($function, (array)$rule['constructorArgs']), |
|||||
517 | 1 | $args |
|||||
518 | ); |
||||||
519 | |||||||
520 | 1 | $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop. |
|||||
521 | 1 | $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs); |
|||||
522 | } else { |
||||||
523 | 2 | $this->instances[$nid] = $instance = $callback(); |
|||||
524 | } |
||||||
525 | |||||||
526 | // Reflect on the instance so that calls can be made against it. |
||||||
527 | 3 | if (is_object($instance)) { |
|||||
528 | 3 | $class = new \ReflectionClass(get_class($instance)); |
|||||
529 | } |
||||||
530 | } else { |
||||||
531 | 28 | $className = empty($rule['class']) ? $nid : $rule['class']; |
|||||
532 | 28 | if (!class_exists($className)) { |
|||||
533 | 1 | throw new NotFoundException("Class $className does not exist.", 404); |
|||||
534 | } |
||||||
535 | 27 | $class = new \ReflectionClass($className); |
|||||
536 | 27 | $constructor = $class->getConstructor(); |
|||||
537 | |||||||
538 | 27 | if ($constructor && $constructor->getNumberOfParameters() > 0) { |
|||||
539 | // Instantiate the object first so that this instance can be used for cyclic dependencies. |
||||||
540 | 26 | $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor(); |
|||||
541 | |||||||
542 | 26 | $constructorArgs = $this->resolveArgs( |
|||||
543 | 26 | $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $rule), |
|||||
0 ignored issues
–
show
The call to
Garden\Container\Container::makeDefaultArgs() has too many arguments starting with $rule .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||||
544 | 25 | $args |
|||||
545 | ); |
||||||
546 | 24 | $constructor->invokeArgs($instance, $constructorArgs); |
|||||
547 | } else { |
||||||
548 | 2 | $this->instances[$nid] = $instance = new $class->name; |
|||||
549 | } |
||||||
550 | } |
||||||
551 | |||||||
552 | // Call subsequent calls on the new object. |
||||||
553 | 28 | if (isset($class) && !empty($rule['calls'])) { |
|||||
554 | 4 | foreach ($rule['calls'] as $call) { |
|||||
555 | 4 | list($methodName, $args) = $call; |
|||||
556 | 4 | $method = $class->getMethod($methodName); |
|||||
557 | |||||||
558 | 4 | $args = $this->resolveArgs( |
|||||
559 | 4 | $this->makeDefaultArgs($method, $args, $rule), |
|||||
560 | 4 | [], |
|||||
561 | 4 | $instance |
|||||
562 | ); |
||||||
563 | |||||||
564 | 4 | $method->invokeArgs($instance, $args); |
|||||
565 | } |
||||||
566 | } |
||||||
567 | |||||||
568 | 28 | return $instance; |
|||||
569 | } |
||||||
570 | |||||||
571 | |||||||
572 | /** |
||||||
573 | * Find the class implemented by an ID. |
||||||
574 | * |
||||||
575 | * This tries to see if a rule exists for a normalized ID and what class it evaluates to. |
||||||
576 | * |
||||||
577 | * @param string $nid The normalized ID to look up. |
||||||
578 | * @return string|null Returns the name of the class associated with the rule or **null** if one could not be found. |
||||||
579 | */ |
||||||
580 | 7 | private function findRuleClass($nid) { |
|||||
581 | 7 | if (!isset($this->rules[$nid])) { |
|||||
582 | 3 | return null; |
|||||
583 | 4 | } elseif (!empty($this->rules[$nid]['aliasOf'])) { |
|||||
584 | return $this->findRuleClass($this->rules[$nid]['aliasOf']); |
||||||
585 | 4 | } elseif (!empty($this->rules[$nid]['class'])) { |
|||||
586 | 2 | return $this->rules[$nid]['class']; |
|||||
587 | } |
||||||
588 | |||||||
589 | 2 | return null; |
|||||
590 | } |
||||||
591 | |||||||
592 | /** |
||||||
593 | * Make an array of default arguments for a given function. |
||||||
594 | * |
||||||
595 | * @param \ReflectionFunctionAbstract $function The function to make the arguments for. |
||||||
596 | * @param array $ruleArgs An array of default arguments specifically for the function. |
||||||
597 | * @return array Returns an array in the form `name => defaultValue`. |
||||||
598 | * @throws NotFoundException If a non-optional class param is reflected and does not exist. |
||||||
599 | */ |
||||||
600 | 70 | private function makeDefaultArgs(\ReflectionFunctionAbstract $function, array $ruleArgs) { |
|||||
601 | 70 | $ruleArgs = array_change_key_case($ruleArgs); |
|||||
602 | 70 | $result = []; |
|||||
603 | |||||||
604 | 70 | $pos = 0; |
|||||
605 | 70 | foreach ($function->getParameters() as $i => $param) { |
|||||
606 | 70 | $name = strtolower($param->name); |
|||||
607 | |||||||
608 | 70 | $reflectedClass = null; |
|||||
0 ignored issues
–
show
|
|||||||
609 | try { |
||||||
610 | 70 | $reflectedClass = $param->getClass(); |
|||||
611 | 4 | } catch (\ReflectionException $e) { |
|||||
612 | // If the class is not found in the autoloader a reflection exception is thrown. |
||||||
613 | // Unless the parameter is optional we will want to rethrow. |
||||||
614 | 4 | if (!$param->isOptional()) { |
|||||
615 | 2 | throw new NotFoundException( |
|||||
616 | 2 | "Could not find required constructor param $name in the autoloader.", |
|||||
617 | 2 | 500, |
|||||
618 | 2 | $e |
|||||
619 | ); |
||||||
620 | } |
||||||
621 | } |
||||||
622 | |||||||
623 | 68 | $hasOrdinalRule = isset($ruleArgs[$pos]); |
|||||
624 | |||||||
625 | 68 | $isMatchingOrdinalReference = false; |
|||||
626 | 68 | $isMatchingOrdinalInstance = false; |
|||||
627 | 68 | if ($hasOrdinalRule && $reflectedClass) { |
|||||
628 | 12 | $ordinalRule = $ruleArgs[$pos]; |
|||||
629 | |||||||
630 | 12 | if ($ordinalRule instanceof Reference) { |
|||||
631 | 7 | $ruleClass = $ordinalRule->getName(); |
|||||
632 | 7 | if (($resolvedRuleClass = $this->findRuleClass($ruleClass)) !== null) { |
|||||
0 ignored issues
–
show
It seems like
$ruleClass can also be of type array ; however, parameter $nid of Garden\Container\Container::findRuleClass() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
633 | 2 | $ruleClass = $resolvedRuleClass; |
|||||
634 | } |
||||||
635 | |||||||
636 | // The argument is a reference that matches the type hint. |
||||||
637 | 7 | $isMatchingOrdinalReference = is_a( |
|||||
638 | 7 | $ruleClass, |
|||||
639 | 7 | $reflectedClass->getName(), |
|||||
640 | 7 | true |
|||||
641 | ); |
||||||
642 | 5 | } elseif (is_object($ordinalRule)) { |
|||||
643 | // The argument is an instance that matches the type hint. |
||||||
644 | 2 | $isMatchingOrdinalInstance = is_a($ordinalRule, $reflectedClass->getName()); |
|||||
645 | } |
||||||
646 | } |
||||||
647 | |||||||
648 | 68 | if (array_key_exists($name, $ruleArgs)) { |
|||||
649 | 5 | $value = $ruleArgs[$name]; |
|||||
650 | } elseif ( |
||||||
651 | 66 | $reflectedClass |
|||||
652 | 66 | && $hasOrdinalRule |
|||||
653 | 66 | && ($isMatchingOrdinalReference|| $isMatchingOrdinalInstance) |
|||||
654 | ) { |
||||||
655 | 7 | $value = $ruleArgs[$pos]; |
|||||
656 | 7 | $pos++; |
|||||
657 | 64 | } elseif ($reflectedClass |
|||||
658 | 64 | && ($reflectedClass->isInstantiable() || isset($this->rules[$reflectedClass->name]) || array_key_exists($reflectedClass->name, $this->instances)) |
|||||
659 | ) { |
||||||
660 | 17 | $value = new DefaultReference($this->normalizeID($reflectedClass->name)); |
|||||
661 | 60 | } elseif ($hasOrdinalRule) { |
|||||
662 | 21 | $value = $ruleArgs[$pos]; |
|||||
663 | 21 | $pos++; |
|||||
664 | 44 | } elseif ($param->isDefaultValueAvailable()) { |
|||||
665 | 37 | $value = $param->getDefaultValue(); |
|||||
666 | 7 | } elseif ($param->isOptional()) { |
|||||
667 | $value = null; |
||||||
668 | } else { |
||||||
669 | 7 | $value = new RequiredParameter($param); |
|||||
670 | } |
||||||
671 | |||||||
672 | 68 | $result[$name] = $value; |
|||||
673 | } |
||||||
674 | |||||||
675 | 68 | return $result; |
|||||
676 | } |
||||||
677 | |||||||
678 | /** |
||||||
679 | * Replace an array of default args with called args. |
||||||
680 | * |
||||||
681 | * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}. |
||||||
682 | * @param array $args The arguments passed into a creation. |
||||||
683 | * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object. |
||||||
684 | * @return array Returns an array suitable to be applied to a function call. |
||||||
685 | * @throws MissingArgumentException Throws an exception when a required parameter is missing. |
||||||
686 | */ |
||||||
687 | 68 | private function resolveArgs(array $defaultArgs, array $args, $instance = null) { |
|||||
688 | // First resolve all passed arguments so their types are known. |
||||||
689 | 68 | $args = array_map( |
|||||
690 | function ($arg) use ($instance) { |
||||||
691 | 17 | return $arg instanceof ReferenceInterface ? $arg->resolve($this, $instance) : $arg; |
|||||
692 | 68 | }, |
|||||
693 | 68 | array_change_key_case($args) |
|||||
694 | ); |
||||||
695 | |||||||
696 | 68 | $pos = 0; |
|||||
697 | 68 | foreach ($defaultArgs as $name => &$default) { |
|||||
698 | 68 | if (array_key_exists($name, $args)) { |
|||||
699 | // This is a named arg and should be used. |
||||||
700 | 2 | $value = $args[$name]; |
|||||
701 | 68 | } elseif (isset($args[$pos]) && (!($default instanceof DefaultReference) || empty($default->getClass()) || is_a($args[$pos], $default->getClass()))) { |
|||||
702 | // There is an arg at this position and it's the same type as the default arg or the default arg is typeless. |
||||||
703 | 15 | $value = $args[$pos]; |
|||||
704 | 15 | $pos++; |
|||||
705 | } else { |
||||||
706 | // There is no passed arg, so use the default arg. |
||||||
707 | 57 | $value = $default; |
|||||
708 | } |
||||||
709 | |||||||
710 | 68 | if ($value instanceof ReferenceInterface) { |
|||||
711 | 21 | $value = $value->resolve($this, $instance); |
|||||
712 | } |
||||||
713 | |||||||
714 | 66 | $default = $value; |
|||||
715 | } |
||||||
716 | |||||||
717 | 66 | return $defaultArgs; |
|||||
718 | } |
||||||
719 | |||||||
720 | /** |
||||||
721 | * Create an instance of a container item. |
||||||
722 | * |
||||||
723 | * This method either creates a new instance or returns an already created shared instance. |
||||||
724 | * |
||||||
725 | * @param string $nid The normalized ID of the container item. |
||||||
726 | * @param array $args Additional arguments to pass to the constructor. |
||||||
727 | * @return object Returns an object instance. |
||||||
728 | */ |
||||||
729 | 77 | private function createInstance($nid, array $args) { |
|||||
730 | 77 | $rule = $this->makeRule($nid); |
|||||
731 | |||||||
732 | // Cache the instance or its factory for future use. |
||||||
733 | 77 | if (empty($rule['shared'])) { |
|||||
734 | 48 | $factory = $this->makeFactory($nid, $rule); |
|||||
735 | 46 | $instance = $factory($args); |
|||||
736 | 45 | $this->factories[$nid] = $factory; |
|||||
737 | } else { |
||||||
738 | 31 | $instance = $this->createSharedInstance($nid, $rule, $args); |
|||||
739 | } |
||||||
740 | 71 | return $instance; |
|||||
741 | } |
||||||
742 | |||||||
743 | /** |
||||||
744 | * Call a callback with argument injection. |
||||||
745 | * |
||||||
746 | * @param callable $callback The callback to call. |
||||||
747 | * @param array $args Additional arguments to pass to the callback. |
||||||
748 | * @return mixed Returns the result of the callback. |
||||||
749 | * @throws ContainerException Throws an exception if the callback cannot be understood. |
||||||
750 | */ |
||||||
751 | 4 | public function call(callable $callback, array $args = []) { |
|||||
752 | 4 | $instance = null; |
|||||
753 | |||||||
754 | 4 | if (is_array($callback)) { |
|||||
755 | 2 | $function = new \ReflectionMethod($callback[0], $callback[1]); |
|||||
756 | |||||||
757 | 2 | if (is_object($callback[0])) { |
|||||
758 | 2 | $instance = $callback[0]; |
|||||
759 | } |
||||||
760 | } else { |
||||||
761 | 2 | $function = new \ReflectionFunction($callback); |
|||||
762 | } |
||||||
763 | |||||||
764 | 4 | $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance); |
|||||
765 | |||||||
766 | 4 | return call_user_func_array($callback, $args); |
|||||
767 | } |
||||||
768 | |||||||
769 | /** |
||||||
770 | * Returns true if the container can return an entry for the given identifier. Returns false otherwise. |
||||||
771 | * |
||||||
772 | * @param string $id Identifier of the entry to look for. |
||||||
773 | * |
||||||
774 | * @return boolean |
||||||
775 | */ |
||||||
776 | 5 | public function has($id) { |
|||||
777 | 5 | $id = $this->normalizeID($id); |
|||||
778 | |||||||
779 | 5 | return isset($this->instances[$id]) || !empty($this->rules[$id]) || class_exists($id); |
|||||
780 | } |
||||||
781 | |||||||
782 | /** |
||||||
783 | * Determines whether a rule has been defined at a given ID. |
||||||
784 | * |
||||||
785 | * @param string $id Identifier of the entry to look for. |
||||||
786 | * @return bool Returns **true** if a rule has been defined or **false** otherwise. |
||||||
787 | */ |
||||||
788 | 4 | public function hasRule($id) { |
|||||
789 | 4 | $id = $this->normalizeID($id); |
|||||
790 | 4 | return !empty($this->rules[$id]); |
|||||
791 | } |
||||||
792 | |||||||
793 | /** |
||||||
794 | * Returns true if the container already has an instance for the given identifier. Returns false otherwise. |
||||||
795 | * |
||||||
796 | * @param string $id Identifier of the entry to look for. |
||||||
797 | * |
||||||
798 | * @return bool |
||||||
799 | */ |
||||||
800 | 1 | public function hasInstance($id) { |
|||||
801 | 1 | $id = $this->normalizeID($id); |
|||||
802 | |||||||
803 | 1 | return isset($this->instances[$id]); |
|||||
804 | } |
||||||
805 | |||||||
806 | /** |
||||||
807 | * Finds an entry of the container by its identifier and returns it. |
||||||
808 | * |
||||||
809 | * @param string $id Identifier of the entry to look for. |
||||||
810 | * |
||||||
811 | * @throws NotFoundException No entry was found for this identifier. |
||||||
812 | * @throws ContainerException Error while retrieving the entry. |
||||||
813 | * |
||||||
814 | * @return mixed Entry. |
||||||
815 | */ |
||||||
816 | 69 | public function get($id) { |
|||||
817 | 69 | return $this->getArgs($id); |
|||||
818 | } |
||||||
819 | |||||||
820 | /** |
||||||
821 | * Determine the reflection information for a callback. |
||||||
822 | * |
||||||
823 | * @param callable $callback The callback to reflect. |
||||||
824 | * @return \ReflectionFunctionAbstract Returns the reflection function for the callback. |
||||||
825 | */ |
||||||
826 | 9 | private function reflectCallback(callable $callback) { |
|||||
827 | 9 | if (is_array($callback)) { |
|||||
828 | 2 | return new \ReflectionMethod($callback[0], $callback[1]); |
|||||
829 | } else { |
||||||
830 | 7 | return new \ReflectionFunction($callback); |
|||||
831 | } |
||||||
832 | } |
||||||
833 | } |
||||||
834 |
This check looks for method names that are not written in camelCase.
In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes
databaseConnectionSeeker
.