Total Complexity | 93 |
Total Lines | 619 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
51 | final class Container implements |
||
52 | ContainerInterface, |
||
53 | BinderInterface, |
||
54 | FactoryInterface, |
||
55 | ResolverInterface, |
||
56 | ScopeInterface |
||
57 | { |
||
58 | /** |
||
59 | * @internal |
||
60 | * @var array |
||
61 | */ |
||
62 | private $bindings = [ |
||
63 | ContainerInterface::class => self::class, |
||
64 | BinderInterface::class => self::class, |
||
65 | FactoryInterface::class => self::class, |
||
66 | ScopeInterface::class => self::class, |
||
67 | ResolverInterface::class => self::class |
||
68 | ]; |
||
69 | |||
70 | /** |
||
71 | * List of classes responsible for handling specific instance or interface. Provides ability to |
||
72 | * delegate container functionality. |
||
73 | * |
||
74 | * @internal |
||
75 | * @var array |
||
76 | */ |
||
77 | private $injectors = []; |
||
78 | |||
79 | /** |
||
80 | * Contains names of all classes which were checked for the available injectors. |
||
81 | * |
||
82 | * @internal |
||
83 | * @var array |
||
84 | */ |
||
85 | private $injectorsCache = []; |
||
86 | |||
87 | /** |
||
88 | * Container constructor. |
||
89 | */ |
||
90 | public function __construct() |
||
91 | { |
||
92 | $this->bindings[static::class] = self::class; |
||
93 | $this->bindings[self::class] = $this; |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Container can not be cloned. |
||
98 | */ |
||
99 | public function __clone() |
||
100 | { |
||
101 | throw new LogicException('Container is not clonable'); |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * {@inheritdoc} |
||
106 | */ |
||
107 | public function has($alias) |
||
108 | { |
||
109 | return array_key_exists($alias, $this->bindings); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * {@inheritdoc} |
||
114 | * |
||
115 | * Context parameter will be passed to class injectors, which makes possible to use this method |
||
116 | * as: |
||
117 | * |
||
118 | * $this->container->get(DatabaseInterface::class, 'default'); |
||
119 | * |
||
120 | * Attention, context ignored when outer container has instance by alias. |
||
121 | * |
||
122 | * @param string|null $context Call context. |
||
123 | * |
||
124 | * @throws ContainerException |
||
125 | * @throws Throwable |
||
126 | */ |
||
127 | public function get($alias, string $context = null) |
||
128 | { |
||
129 | if ($alias instanceof Autowire) { |
||
130 | return $alias->resolve($this); |
||
131 | } |
||
132 | |||
133 | return $this->make($alias, [], $context); |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * {@inheritdoc} |
||
138 | * |
||
139 | * @param string|null $context Related to parameter caused injection if any. |
||
140 | * |
||
141 | * @throws Throwable |
||
142 | */ |
||
143 | public function make(string $alias, array $parameters = [], string $context = null) |
||
144 | { |
||
145 | if (!isset($this->bindings[$alias])) { |
||
146 | //No direct instructions how to construct class, make is automatically |
||
147 | return $this->autowire($alias, $parameters, $context); |
||
148 | } |
||
149 | |||
150 | $binding = $this->bindings[$alias]; |
||
151 | if (is_object($binding)) { |
||
152 | //When binding is instance, assuming singleton |
||
153 | return $binding; |
||
154 | } |
||
155 | |||
156 | if (is_string($binding)) { |
||
157 | //Binding is pointing to something else |
||
158 | return $this->make($binding, $parameters, $context); |
||
159 | } |
||
160 | |||
161 | unset($this->bindings[$alias]); |
||
162 | try { |
||
163 | if ($binding[0] === $alias) { |
||
164 | $instance = $this->autowire($alias, $parameters, $context); |
||
165 | } else { |
||
166 | $instance = $this->evaluateBinding($alias, $binding[0], $parameters, $context); |
||
167 | } |
||
168 | } finally { |
||
169 | $this->bindings[$alias] = $binding; |
||
170 | } |
||
171 | |||
172 | if ($binding[1]) { |
||
173 | //Indicates singleton |
||
174 | $this->bindings[$alias] = $instance; |
||
|
|||
175 | } |
||
176 | |||
177 | return $instance; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * {@inheritdoc} |
||
182 | * |
||
183 | * @param string $context |
||
184 | * |
||
185 | * @throws Throwable |
||
186 | */ |
||
187 | public function resolveArguments( |
||
188 | ContextFunction $reflection, |
||
189 | array $parameters = [], |
||
190 | string $context = null |
||
191 | ): array { |
||
192 | $arguments = []; |
||
193 | foreach ($reflection->getParameters() as $parameter) { |
||
194 | try { |
||
195 | //Information we need to know about argument in order to resolve it's value |
||
196 | $name = $parameter->getName(); |
||
197 | $class = $parameter->getClass(); |
||
198 | } catch (Throwable $e) { |
||
199 | //Possibly invalid class definition or syntax error |
||
200 | $location = $reflection->getName(); |
||
201 | if ($reflection instanceof ReflectionMethod) { |
||
202 | $location = "{$reflection->getDeclaringClass()->getName()}->{$location}"; |
||
203 | } |
||
204 | //Possibly invalid class definition or syntax error |
||
205 | throw new ContainerException( |
||
206 | "Unable to resolve `{$parameter->getName()}` in {$location}: " . $e->getMessage(), |
||
207 | $e->getCode(), |
||
208 | $e |
||
209 | ); |
||
210 | } |
||
211 | |||
212 | if (isset($parameters[$name]) && is_object($parameters[$name])) { |
||
213 | if ($parameters[$name] instanceof Autowire) { |
||
214 | //Supplied by user as late dependency |
||
215 | $arguments[] = $parameters[$name]->resolve($this); |
||
216 | } else { |
||
217 | //Supplied by user as object |
||
218 | $arguments[] = $parameters[$name]; |
||
219 | } |
||
220 | continue; |
||
221 | } |
||
222 | |||
223 | // no declared type or scalar type or array |
||
224 | if (!isset($class)) { |
||
225 | //Provided from outside |
||
226 | if (array_key_exists($name, $parameters)) { |
||
227 | //Make sure it's properly typed |
||
228 | $this->assertType($parameter, $reflection, $parameters[$name]); |
||
229 | $arguments[] = $parameters[$name]; |
||
230 | continue; |
||
231 | } |
||
232 | |||
233 | if ($parameter->isDefaultValueAvailable()) { |
||
234 | //Default value |
||
235 | $arguments[] = $parameter->getDefaultValue(); |
||
236 | continue; |
||
237 | } |
||
238 | |||
239 | //Unable to resolve scalar argument value |
||
240 | throw new ArgumentException($parameter, $reflection); |
||
241 | } |
||
242 | |||
243 | try { |
||
244 | //Requesting for contextual dependency |
||
245 | $arguments[] = $this->get($class->getName(), $name); |
||
246 | continue; |
||
247 | } catch (AutowireException $e) { |
||
248 | if ($parameter->isOptional()) { |
||
249 | //This is optional dependency, skip |
||
250 | $arguments[] = null; |
||
251 | continue; |
||
252 | } |
||
253 | |||
254 | throw $e; |
||
255 | } |
||
256 | } |
||
257 | |||
258 | return $arguments; |
||
259 | } |
||
260 | |||
261 | /** |
||
262 | * @inheritdoc |
||
263 | */ |
||
264 | public function runScope(array $bindings, callable $scope) |
||
265 | { |
||
266 | $cleanup = $previous = []; |
||
267 | foreach ($bindings as $alias => $resolver) { |
||
268 | if (isset($this->bindings[$alias])) { |
||
269 | $previous[$alias] = $this->bindings[$alias]; |
||
270 | } else { |
||
271 | $cleanup[] = $alias; |
||
272 | } |
||
273 | |||
274 | $this->bind($alias, $resolver); |
||
275 | } |
||
276 | |||
277 | try { |
||
278 | if (ContainerScope::getContainer() !== $this) { |
||
279 | return ContainerScope::runScope($this, $scope); |
||
280 | } |
||
281 | |||
282 | return $scope(); |
||
283 | } finally { |
||
284 | foreach (array_reverse($previous) as $alias => $resolver) { |
||
285 | $this->bindings[$alias] = $resolver; |
||
286 | } |
||
287 | |||
288 | foreach ($cleanup as $alias) { |
||
289 | unset($this->bindings[$alias]); |
||
290 | } |
||
291 | } |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * Bind value resolver to container alias. Resolver can be class name (will be constructed |
||
296 | * for each method call), function array or Closure (executed every call). Only object resolvers |
||
297 | * supported by this method. |
||
298 | * |
||
299 | * @param string $alias |
||
300 | * @param string|array|callable $resolver |
||
301 | */ |
||
302 | public function bind(string $alias, $resolver): void |
||
303 | { |
||
304 | if (is_array($resolver) || $resolver instanceof Closure || $resolver instanceof Autowire) { |
||
305 | // array means = execute me, false = not singleton |
||
306 | $this->bindings[$alias] = [$resolver, false]; |
||
307 | return; |
||
308 | } |
||
309 | |||
310 | $this->bindings[$alias] = $resolver; |
||
311 | } |
||
312 | |||
313 | /** |
||
314 | * Bind value resolver to container alias to be executed as cached. Resolver can be class name |
||
315 | * (will be constructed only once), function array or Closure (executed only once call). |
||
316 | * |
||
317 | * @param string $alias |
||
318 | * @param string|array|callable $resolver |
||
319 | */ |
||
320 | public function bindSingleton(string $alias, $resolver): void |
||
321 | { |
||
322 | if (is_object($resolver) && !$resolver instanceof Closure && !$resolver instanceof Autowire) { |
||
323 | // direct binding to an instance |
||
324 | $this->bindings[$alias] = $resolver; |
||
325 | return; |
||
326 | } |
||
327 | |||
328 | $this->bindings[$alias] = [$resolver, true]; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Check if alias points to constructed instance (singleton). |
||
333 | * |
||
334 | * @param string $alias |
||
335 | * @return bool |
||
336 | */ |
||
337 | public function hasInstance(string $alias): bool |
||
338 | { |
||
339 | if (!$this->has($alias)) { |
||
340 | return false; |
||
341 | } |
||
342 | |||
343 | while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) { |
||
344 | //Checking alias tree |
||
345 | $alias = $this->bindings[$alias]; |
||
346 | } |
||
347 | |||
348 | return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]); |
||
349 | } |
||
350 | |||
351 | /** |
||
352 | * @param string $alias |
||
353 | */ |
||
354 | public function removeBinding(string $alias): void |
||
355 | { |
||
356 | unset($this->bindings[$alias]); |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * Bind class or class interface to the injector source (InjectorInterface). |
||
361 | * |
||
362 | * @param string $class |
||
363 | * @param string $injector |
||
364 | * @return self |
||
365 | */ |
||
366 | public function bindInjector(string $class, string $injector): Container |
||
367 | { |
||
368 | $this->injectors[$class] = $injector; |
||
369 | $this->injectorsCache = []; |
||
370 | |||
371 | return $this; |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * @param string $class |
||
376 | */ |
||
377 | public function removeInjector(string $class): void |
||
378 | { |
||
379 | unset($this->injectors[$class]); |
||
380 | $this->injectorsCache = []; |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Every declared Container binding. Must not be used in production code due container format is |
||
385 | * vary. |
||
386 | * |
||
387 | * @return array |
||
388 | */ |
||
389 | public function getBindings(): array |
||
390 | { |
||
391 | return $this->bindings; |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Every binded injector. |
||
396 | * |
||
397 | * @return array |
||
398 | */ |
||
399 | public function getInjectors(): array |
||
400 | { |
||
401 | return $this->injectors; |
||
402 | } |
||
403 | |||
404 | /** |
||
405 | * Automatically create class. |
||
406 | * |
||
407 | * @param string $class |
||
408 | * @param array $parameters |
||
409 | * @param string $context |
||
410 | * @return object |
||
411 | * |
||
412 | * @throws AutowireException |
||
413 | * @throws Throwable |
||
414 | */ |
||
415 | protected function autowire(string $class, array $parameters, string $context = null) |
||
416 | { |
||
417 | if (!class_exists($class)) { |
||
418 | throw new NotFoundException(sprintf("Undefined class or binding '%s'", $class)); |
||
419 | } |
||
420 | |||
421 | // automatically create instance |
||
422 | $instance = $this->createInstance($class, $parameters, $context); |
||
423 | |||
424 | // apply registration functions to created instance |
||
425 | return $this->registerInstance($instance, $parameters); |
||
426 | } |
||
427 | |||
428 | /** |
||
429 | * Register instance in container, might perform methods like auto-singletons, log populations |
||
430 | * and etc. Can be extended. |
||
431 | * |
||
432 | * @param object $instance Created object. |
||
433 | * @param array $parameters Parameters which been passed with created instance. |
||
434 | * @return object |
||
435 | */ |
||
436 | private function registerInstance($instance, array $parameters) |
||
437 | { |
||
438 | //Declarative singletons (only when class received via direct get) |
||
439 | if ($parameters === [] && $instance instanceof SingletonInterface) { |
||
440 | $alias = get_class($instance); |
||
441 | if (!isset($this->bindings[$alias])) { |
||
442 | $this->bindings[$alias] = $instance; |
||
443 | } |
||
444 | } |
||
445 | |||
446 | //Your code can go here (for example LoggerAwareInterface, custom hydration and etc) |
||
447 | return $instance; |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * @param string $alias |
||
452 | * @param mixed $target Value binded by user. |
||
453 | * @param array $parameters |
||
454 | * @param string|null $context |
||
455 | * @return mixed|null|object |
||
456 | * |
||
457 | * @throws ContainerExceptionInterface |
||
458 | * @throws Throwable |
||
459 | */ |
||
460 | private function evaluateBinding( |
||
461 | string $alias, |
||
462 | $target, |
||
463 | array $parameters, |
||
464 | string $context = null |
||
465 | ) { |
||
466 | if (is_string($target)) { |
||
467 | //Reference |
||
468 | return $this->make($target, $parameters, $context); |
||
469 | } |
||
470 | |||
471 | if ($target instanceof Autowire) { |
||
472 | return $target->resolve($this, $parameters); |
||
473 | } |
||
474 | |||
475 | if ($target instanceof Closure) { |
||
476 | try { |
||
477 | $reflection = new ReflectionFunction($target); |
||
478 | } catch (ReflectionException $e) { |
||
479 | throw new ContainerException($e->getMessage(), $e->getCode(), $e); |
||
480 | } |
||
481 | |||
482 | //Invoking Closure with resolved arguments |
||
483 | return $reflection->invokeArgs( |
||
484 | $this->resolveArguments($reflection, $parameters, $context) |
||
485 | ); |
||
486 | } |
||
487 | |||
488 | if (is_array($target) && isset($target[1])) { |
||
489 | //In a form of resolver and method |
||
490 | [$resolver, $method] = $target; |
||
491 | |||
492 | //Resolver instance (i.e. [ClassName::class, 'method']) |
||
493 | $resolver = $this->get($resolver); |
||
494 | |||
495 | try { |
||
496 | $method = new ReflectionMethod($resolver, $method); |
||
497 | } catch (ReflectionException $e) { |
||
498 | throw new ContainerException($e->getMessage(), $e->getCode(), $e); |
||
499 | } |
||
500 | |||
501 | $method->setAccessible(true); |
||
502 | |||
503 | //Invoking factory method with resolved arguments |
||
504 | return $method->invokeArgs( |
||
505 | $resolver, |
||
506 | $this->resolveArguments($method, $parameters, $context) |
||
507 | ); |
||
508 | } |
||
509 | |||
510 | throw new ContainerException(sprintf("Invalid binding for '%s'", $alias)); |
||
511 | } |
||
512 | |||
513 | /** |
||
514 | * Create instance of desired class. |
||
515 | * |
||
516 | * @param string $class |
||
517 | * @param array $parameters Constructor parameters. |
||
518 | * @param string|null $context |
||
519 | * @return object |
||
520 | * |
||
521 | * @throws ContainerException |
||
522 | * @throws Throwable |
||
523 | */ |
||
524 | private function createInstance(string $class, array $parameters, string $context = null) |
||
525 | { |
||
526 | try { |
||
527 | $reflection = new ReflectionClass($class); |
||
528 | } catch (ReflectionException $e) { |
||
529 | throw new ContainerException($e->getMessage(), $e->getCode(), $e); |
||
530 | } |
||
531 | |||
532 | //We have to construct class using external injector when we know exact context |
||
533 | if ($parameters === [] && $this->checkInjector($reflection)) { |
||
534 | $injector = $this->injectors[$reflection->getName()]; |
||
535 | |||
536 | $instance = null; |
||
537 | try { |
||
538 | /** @var InjectorInterface $injectorInstance */ |
||
539 | $injectorInstance = $this->get($injector); |
||
540 | |||
541 | if (!$injectorInstance instanceof InjectorInterface) { |
||
542 | throw new InjectionException( |
||
543 | sprintf( |
||
544 | "Class '%s' must be an instance of InjectorInterface for '%s'", |
||
545 | get_class($injectorInstance), |
||
546 | $reflection->getName() |
||
547 | ) |
||
548 | ); |
||
549 | } |
||
550 | |||
551 | $instance = $injectorInstance->createInjection($reflection, $context); |
||
552 | if (!$reflection->isInstance($instance)) { |
||
553 | throw new InjectionException( |
||
554 | sprintf( |
||
555 | "Invalid injection response for '%s'", |
||
556 | $reflection->getName() |
||
557 | ) |
||
558 | ); |
||
559 | } |
||
560 | } finally { |
||
561 | $this->injectors[$reflection->getName()] = $injector; |
||
562 | } |
||
563 | |||
564 | return $instance; |
||
565 | } |
||
566 | |||
567 | if (!$reflection->isInstantiable()) { |
||
568 | throw new ContainerException(sprintf("Class '%s' can not be constructed", $class)); |
||
569 | } |
||
570 | |||
571 | $constructor = $reflection->getConstructor(); |
||
572 | |||
573 | if ($constructor !== null) { |
||
574 | // Using constructor with resolved arguments |
||
575 | $instance = $reflection->newInstanceArgs($this->resolveArguments($constructor, $parameters)); |
||
576 | } else { |
||
577 | // No constructor specified |
||
578 | $instance = $reflection->newInstance(); |
||
579 | } |
||
580 | |||
581 | return $instance; |
||
582 | } |
||
583 | |||
584 | /** |
||
585 | * Checks if given class has associated injector. |
||
586 | * |
||
587 | * @param ReflectionClass $reflection |
||
588 | * @return bool |
||
589 | */ |
||
590 | private function checkInjector(ReflectionClass $reflection): bool |
||
591 | { |
||
592 | $class = $reflection->getName(); |
||
593 | if (array_key_exists($class, $this->injectors)) { |
||
594 | return $this->injectors[$class] !== null; |
||
595 | } |
||
596 | |||
597 | if ( |
||
598 | $reflection->implementsInterface(InjectableInterface::class) |
||
599 | && $reflection->hasConstant('INJECTOR') |
||
600 | ) { |
||
601 | $this->injectors[$class] = $reflection->getConstant('INJECTOR'); |
||
602 | return true; |
||
603 | } |
||
604 | |||
605 | if (!isset($this->injectorsCache[$class])) { |
||
606 | $this->injectorsCache[$class] = null; |
||
607 | |||
608 | // check interfaces |
||
609 | foreach ($this->injectors as $target => $injector) { |
||
610 | if ( |
||
611 | class_exists($target, true) |
||
612 | && $reflection->isSubclassOf($target) |
||
613 | ) { |
||
614 | $this->injectors[$class] = $injector; |
||
615 | return true; |
||
616 | } |
||
617 | |||
618 | if ( |
||
619 | interface_exists($target, true) |
||
620 | && $reflection->implementsInterface($target) |
||
621 | ) { |
||
622 | $this->injectors[$class] = $injector; |
||
623 | return true; |
||
624 | } |
||
625 | } |
||
626 | } |
||
627 | |||
628 | return false; |
||
629 | } |
||
630 | |||
631 | /** |
||
632 | * Assert that given value are matched parameter type. |
||
633 | * |
||
634 | * @param ReflectionParameter $parameter |
||
635 | * @param ContextFunction $context |
||
636 | * @param mixed $value |
||
637 | * |
||
638 | * @throws ArgumentException |
||
639 | * @throws ReflectionException |
||
640 | */ |
||
641 | private function assertType(ReflectionParameter $parameter, ContextFunction $context, $value): void |
||
670 | } |
||
671 | } |
||
672 | } |
||
673 |