Passed
Branch master (2d2b40)
by Divine Niiquaye
02:25
created

Container::fallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI;
19
20
use Nette\Utils\Helpers;
21
use Psr\Container\ContainerExceptionInterface;
22
use Psr\Container\ContainerInterface;
23
use Psr\Container\NotFoundExceptionInterface;
24
use Rade\DI\Exceptions\CircularReferenceException;
25
use Rade\DI\Exceptions\ContainerResolutionException;
26
use Rade\DI\Exceptions\FrozenServiceException;
27
use Rade\DI\Exceptions\NotFoundServiceException;
28
use Rade\DI\Resolvers\AutowireValueResolver;
29
use Rade\DI\Services\ServiceProviderInterface;
30
use Symfony\Component\Config\Definition\Processor;
31
use Symfony\Contracts\Service\ResetInterface;
32
33
/**
34
 * Dependency injection container.
35
 *
36
 * @author Divine Niiquaye Ibok <[email protected]>
37
 */
38
class Container implements \ArrayAccess, ContainerInterface, ResetInterface
39
{
40
    use Traits\AutowireTrait;
41
42
    protected array $types = [
43
        ContainerInterface::class => ['container'],
44
        Container::class => ['container'],
45
    ];
46
47
    /** @var array<string,string> internal cached services */
48
    protected array $methodsMap = ['container' => 'getServiceContainer'];
49
50
    /** @var array<string,mixed> For handling a global config around services */
51
    public array $parameters = [];
52
53
    /** @var array<string,mixed> A list of already loaded services (this act as a local cache) */
54
    private static array $services;
55
56
    /** @var ServiceProviderInterface[] A list of service providers */
57
    protected array $providers = [];
58
59
    /** @var ContainerInterface[] A list of fallback PSR-11 containers */
60
    protected array $fallback = [];
61
62
    /**
63
     * Instantiates the container.
64
     */
65 75
    public function __construct()
66
    {
67 75
        static::$services = [];
0 ignored issues
show
Bug introduced by
Since $services is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $services to at least protected.
Loading history...
68
69
        // Incase this class it extended ...
70 75
        if (static::class !== __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class !== __CLASS__ is always false.
Loading history...
71 5
            $this->types += [static::class => ['container']];
72
        }
73
74 75
        $this->resolver = new AutowireValueResolver($this, $this->types);
75 75
    }
76
77
    /**
78
     * Container can not be cloned.
79
     */
80 1
    public function __clone()
81
    {
82 1
        throw new \LogicException('Container is not clonable');
83
    }
84
85
    /**
86
     * Dynamically access container services.
87
     *
88
     * @param string $key
89
     *
90
     * @return mixed
91
     */
92 2
    public function __get($key)
93
    {
94 2
        return $this->offsetGet($key);
95
    }
96
97
    /**
98
     * Dynamically set container services.
99
     *
100
     * @param string $key
101
     * @param object $value
102
     */
103 1
    public function __set($key, $value): void
104
    {
105 1
        $this->offsetSet($key, $value);
106 1
    }
107
108
    /**
109
     * Sets a new service to a unique identifier.
110
     *
111
     * @param string $offset The unique identifier for the parameter or object
112
     * @param mixed  $value  The value of the service assign to the $offset
113
     *
114
     * @throws FrozenServiceException Prevent override of a frozen service
115
     */
116 58
    public function offsetSet($offset, $value): void
117
    {
118 58
        $this->set($offset, $value, true);
119 58
    }
120
121
    /**
122
     * Gets a registered service definition.
123
     *
124
     * @param string $offset The unique identifier for the service
125
     *
126
     * @throws NotFoundServiceException If the identifier is not defined
127
     *
128
     * @return mixed The value of the service
129
     */
130 59
    public function offsetGet($offset)
131
    {
132 59
        return self::$services[$offset] ?? $this->raw[$offset] ?? $this->fallback[$offset]
133 59
            ?? ($this->factories[$offset] ?? [$this, 'getService'])($offset);
134
    }
135
136
    /**
137
     * Checks if a service is set.
138
     *
139
     * @param string $offset The unique identifier for the service
140
     *
141
     * @return bool
142
     */
143 16
    public function offsetExists($offset): bool
144
    {
145 16
        if ($this->keys[$offset] ?? isset($this->methodsMap[$offset])) {
146 13
            return true;
147
        }
148
149 10
        if ([] !== $this->fallback) {
150 2
            if (isset($this->fallback[$offset])) {
151 1
                return true;
152
            }
153
154 2
            foreach ($this->fallback as $container) {
155
                try {
156 2
                    return $container->has($offset);
157 1
                } catch (NotFoundExceptionInterface $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
158
                }
159
            }
160
        }
161
162 9
        return isset($this->aliases[$offset]) ? $this->offsetExists($this->aliases[$offset]) : false;
163
    }
164
165
    /**
166
     * Unsets a service by given offset.
167
     *
168
     * @param string $offset The unique identifier for service definition
169
     */
170 7
    public function offsetUnset($offset): void
171
    {
172 7
        if ($this->offsetExists($offset)) {
173 7
            unset($this->values[$offset], $this->factories[$offset], $this->frozen[$offset], $this->raw[$offset], $this->keys[$offset], self::$services[$offset]);
174
        }
175 7
    }
176
177
    /**
178
     * Marks an alias id to service id.
179
     *
180
     * @param string $id The alias id
181
     * @param string $serviceId The registered service id
182
     *
183
     * @throws ContainerResolutionException Service id is not found in container
184
     */
185 7
    public function alias(string $id, string $serviceId): void
186
    {
187 7
        if ($id === $serviceId) {
188 1
            throw new \LogicException("[{$id}] is aliased to itself.");
189
        }
190
191 6
        if (!$this->offsetExists($serviceId)) {
192 2
            throw new ContainerResolutionException("Service id '{$serviceId}' is not found in container");
193
        }
194
195 4
        $this->aliases[$id] = $this->aliases[$serviceId] ?? $serviceId;
196 4
    }
197
198
    /**
199
     * Assign a set of tags to service(s).
200
     *
201
     * @param string[]|string         $serviceIds
202
     * @param array<int|string,mixed> $tags
203
     */
204 4
    public function tag($serviceIds, array $tags): void
205
    {
206 4
        foreach ((array) $serviceIds as $service) {
207 4
            foreach ($tags as $tag => $attributes) {
208
                // Exchange values if $tag is an integer
209 4
                if (\is_int($tmp = $tag)) {
210 3
                    $tag = $attributes;
211 3
                    $attributes = $tmp;
212
                }
213
214 4
                $this->tags[$service][$tag] = $attributes;
215
            }
216
        }
217 4
    }
218
219
    /**
220
     * Resolve all of the bindings for a given tag.
221
     *
222
     * @param string $tag
223
     *
224
     * @return mixed[] of [service, attributes]
225
     */
226 4
    public function tagged(string $tag): array
227
    {
228 4
        $tags = [];
229
230 4
        foreach ($this->tags as $service => $tagged) {
231 4
            if (isset($tagged[$tag])) {
232 4
                $tags[] = [$this->get($service), $tagged[$tag]];
233
            }
234
        }
235
236 4
        return $tags;
237
    }
238
239
    /**
240
     * Wraps a class string or callable with a `call` method.
241
     *
242
     * This is useful when you want to autowire a callable or class string lazily.
243
     *
244
     * @param callable|string $definition A class string or a callable
245
     */
246 16
    public function lazy($definition): ScopedDefinition
247
    {
248 16
        return new ScopedDefinition($definition, ScopedDefinition::LAZY);
249
    }
250
251
    /**
252
     * Marks a callable as being a factory service.
253
     *
254
     * @param callable $callable A service definition to be used as a factory
255
     */
256 6
    public function factory($callable): ScopedDefinition
257
    {
258 6
        return new ScopedDefinition(fn () => $this->call($callable));
259
    }
260
261
    /**
262
     * Marks a definition from being interpreted as a service.
263
     *
264
     * @param mixed $definition from being evaluated
265
     */
266 14
    public function raw($definition): ScopedDefinition
267
    {
268 14
        return new ScopedDefinition($definition, ScopedDefinition::RAW);
269
    }
270
271
    /**
272
     * Extends an object definition.
273
     *
274
     * Useful when you want to extend an existing object definition,
275
     * without necessarily loading that object.
276
     *
277
     * @param string   $id    The unique identifier for the object
278
     * @param callable $scope A service definition to extend the original
279
     *
280
     * @throws NotFoundServiceException If the identifier is not defined
281
     * @throws FrozenServiceException   If the service is frozen
282
     * @throws CircularReferenceException If infinite loop among service is detected
283
     *
284
     * @return mixed The wrapped scope
285
     */
286 9
    public function extend(string $id, callable $scope)
287
    {
288 9
        $this->extendable($id);
289
290
        // Extended factories should always return new instance ...
291 5
        if (isset($this->factories[$id])) {
292 2
            $factory = $this->factories[$id];
293
294 2
            return $this->factories[$id] = fn () => $scope($factory(), $this);
295
        }
296
297 4
        if (\is_object($extended = $this->values[$id] ?? null)) {
298
            // This is a hungry implementation to autowire $extended if it's an object.
299 4
            $this->autowireService($id, $extended);
300
301 4
            if (\is_callable($extended)) {
302 4
                $extended = $this->doCreate($id, $this->values[$id]);
303
304
                // Unfreeze service if frozen ...
305 3
                unset($this->frozen[$id]);
306
            }
307
        }
308
309
        // We bare in mind that $extended could return anyting, and does want to exist in $this->values.
310 3
        return $this->values[$id] = $scope($extended, $this);
311
    }
312
313
    /**
314
     * Check if servie if can be extended.
315
     *
316
     * @param string $id
317
     *
318
     * @throws NotFoundServiceException If the identifier is not defined
319
     * @throws FrozenServiceException   If the service is frozen
320
     *
321
     * @return bool
322
     */
323 9
    public function extendable(string $id): bool
324
    {
325 9
        if (!isset($this->keys[$id])) {
326 1
            throw new NotFoundServiceException(sprintf('Identifier "%s" is not defined.', $id));
327
        }
328
329 8
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
330 2
            throw new FrozenServiceException($id);
331
        }
332
333 6
        if (isset($this->raw[$id])) {
334 1
            throw new ContainerResolutionException("Service definition '{$id}' cannot be extended, was not meant to be resolved.");
335
        }
336
337 5
        return true;
338
    }
339
340
    /**
341
     * Returns all defined value names.
342
     *
343
     * @return string[] An array of value names
344
     */
345 18
    public function keys(): array
346
    {
347 18
        return \array_keys($this->keys + $this->methodsMap);
348
    }
349
350
    /**
351
     * Resets the container
352
     */
353 3
    public function reset(): void
354
    {
355 3
        foreach ($this->values as $id => $service) {
356 1
            if ($service instanceof ResetInterface) {
357 1
                $service->reset();
358
            }
359
360 1
            unset($this->values[$id], $this->factories[$id], $this->raw[$id], $this->keys[$id], $this->frozen[$id], self::$services[$id]);
361
        }
362
363
        // A container such as Symfony DI support reset ...
364 3
        foreach ($this->fallback as $fallback) {
365 1
            if ($fallback instanceof ResetInterface) {
366 1
                $fallback->reset();
367
            }
368
        }
369
370 3
        $this->tags = $this->aliases = self::$services = $this->fallback = [];
371 3
    }
372
373
    /**
374
     * {@inheritdoc}
375
     *
376
     * @param string                  $id — Identifier of the entry to look for.
377
     * @param array<int|string,mixed> $arguments
378
     */
379 17
    public function get($id, array $arguments = [])
380
    {
381
        // If service has already been requested and cached ...
382 17
        if (isset(self::$services[$id])) {
383 2
            return self::$services[$id];
384
        }
385
386
        try {
387 17
            if (\is_callable($protected = $this->raw[$id] ?? null) && [] !== $arguments) {
388 1
                $protected = $this->call($protected, $arguments);
389
            }
390
391 17
            return $protected ?? $this->fallback[$id] ?? ($this->factories[$id] ?? [$this, 'getService'])($id);
392 15
        } catch (NotFoundServiceException $serviceError) {
393
            try {
394 15
                return $this->resolver->getByType($id);
395 9
            } catch (NotFoundServiceException $typeError) {
396 9
                if (\class_exists($id)) {
397
                    try {
398 7
                        return $this->autowireClass($id, $arguments);
399 1
                    } catch (ContainerResolutionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
400
                    }
401
                }
402
            }
403
404 3
            if (isset($this->aliases[$id])) {
405 1
                return $this->get($this->aliases[$id], $arguments);
406
            }
407
408 3
            throw $serviceError;
409
        }
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415 4
    public function has($id): bool
416
    {
417 4
        if ($this->offsetExists($id)) {
418 3
            return true;
419
        }
420
421 2
        throw new NotFoundServiceException(sprintf('Identifier "%s" is not defined.', $id));
422
    }
423
424
    /**
425
     * Set a service definition
426
     *
427
     * @param object $definition
428
     *
429
     * @throws FrozenServiceException Prevent override of a frozen service
430
     */
431 59
    public function set(string $id, object $definition, bool $autowire = false): void
432
    {
433 59
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
434 1
            throw new FrozenServiceException($id);
435
        }
436
437
        // Incase new service definition exists in aliases.
438 59
        unset($this->aliases[$id]);
439
440 59
        if (!$definition instanceof ScopedDefinition) {
441
           // Resolving the closure of the service to return it's type hint or class.
442 44
            $this->values[$id] = !$autowire ? $definition : $this->autowireService($id, $definition);
443
        } else {
444
            // Lazy class-string service $definition
445 34
            if (\class_exists($property = $definition->property)) {
446 17
                if ($autowire) {
447 16
                    $this->resolver->autowire($id, [$property]);
448
                }
449 17
                $property = 'values';
450
            }
451
452 34
            $this->{$property}[$id] = $definition->service;
453
        }
454
455 59
        $this->keys[$id] = true;
456 59
    }
457
458
    /**
459
     * Registers a service provider.
460
     *
461
     * @param ServiceProviderInterface $provider A ServiceProviderInterface instance
462
     * @param array                    $values   An array of values that customizes the provider
463
     *
464
     * @return static
465
     */
466 1
    public function register(ServiceProviderInterface $provider, array $values = [])
467
    {
468 1
        $this->providers[] = $provider;
469
470 1
        if ([] !== $values && $provider instanceof Services\ConfigurationInterface) {
471 1
            $id = $provider->getName();
472 1
            $process = [new Processor(), 'processConfiguration'];
473
474 1
            $this->parameters[$id] = $process($provider, isset($values[$id]) ? $values : [$id => $values]);
475
        }
476
477
        // If service provider depends on other providers ...
478 1
        if ($provider instanceof Services\DependedInterface) {
479 1
            foreach ($provider->dependencies() as $dependency) {
480 1
                $dependency = $this->autowireClass($dependency, []);
481
482 1
                if ($dependency instanceof ServiceProviderInterface) {
483 1
                    $this->register($dependency);
484
                }
485
            }
486
        }
487
488 1
        $provider->register($this);
489
490 1
        return $this;
491
    }
492
493
    /**
494
     * Register a PSR-11 fallback container.
495
     *
496
     * @param ContainerInterface $fallback
497
     *
498
     * @return static
499
     */
500 4
    public function fallback(ContainerInterface $fallback)
501
    {
502 4
        $this->fallback[$name = \get_class($fallback)] = $fallback;
503
504
        // Autowire $fallback, incase services parametes
505
        // requires it, container is able to serve it.
506 4
        $this->resolver->autowire($name, [$name]);
507
508 4
        return $this;
509
    }
510
511
    /**
512
     * @internal
513
     *
514
     * Get the mapped service container instance
515
     */
516 21
    protected function getServiceContainer(): self
517
    {
518 21
        return $this;
519
    }
520
521
    /**
522
     * Build an entry of the container by its name.
523
     *
524
     * @param string $id
525
     *
526
     * @throws CircularReferenceException
527
     * @throws NotFoundServiceException
528
     *
529
     * @return mixed
530
     */
531 53
    protected function getService(string $id)
532
    {
533 53
        if (isset($this->methodsMap[$id])) {
534 24
            return self::$services[$id] = $this->{$this->methodsMap[$id]}();
535 53
        } elseif (isset($this->values[$id])) {
536
            // If we found the real instance of $service, lets cache that ...
537 41
            if (!\is_callable($service = $this->values[$id])) {
538 22
                $this->frozen[$id] ??= true;
539
540 22
                return self::$services[$id] = $service;
541
            }
542
543
            // we have to create the object and avoid infinite lopp.
544 32
            return $this->doCreate($id, $service);
545
        }
546
547 20
        if ([] !== $this->fallback) {
548
            // A bug is discovered here, if fallback is a dynamically autowired container like this one.
549
            // Instead a return of fallback container, main container should be used.
550 3
            if ($id === ContainerInterface::class) {
551 1
                return $this;
552
            }
553
554 3
            foreach ($this->fallback as $container) {
555
                try {
556 3
                    return self::$services[$id] = $container->get($id);
557 2
                } catch (ContainerExceptionInterface $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
558
                }
559
            }
560 18
        } elseif (isset($this->aliases[$id])) {
561 2
            return $this->offsetGet($this->aliases[$id]);
562
        }
563
564 17
        if (null !== $suggest = Helpers::getSuggestion($this->keys(), $id)) {
565 1
            $suggest = " Did you mean: \"{$suggest}\" ?";
566
        }
567
568 17
        throw new NotFoundServiceException(\sprintf('Identifier "%s" is not defined.' . $suggest, $id), 0, $e ?? null);
569
    }
570
571
    /**
572
     * This is a performance sensitive method, please do not modify.
573
     *
574
     * @return mixed
575
     */
576 34
    protected function doCreate(string $id, callable $service)
577
    {
578
        // Checking if circular reference exists ...
579 34
        if (isset($this->loading[$id])) {
580 2
            throw new CircularReferenceException($id, [...\array_keys($this->loading), $id]);
581
        }
582 34
        $this->loading[$id] = true;
583
584
        try {
585 34
            $this->values[$id] = $this->call($service);
586 27
            $this->frozen[$id] = true; // Freeze resolved service ...
587
588 27
            return $this->values[$id];
589
        } finally {
590 34
            unset($this->loading[$id]);
591
        }
592
    }
593
}
594