Passed
Push — master ( db5893...1bb971 )
by Alexander
07:58 queued 05:11
created

Container::get()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 10.1371

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 18
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 33
ccs 16
cts 18
cp 0.8889
crap 10.1371
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Di;
6
7
use Closure;
8
use Psr\Container\ContainerInterface;
9
use Yiisoft\Definitions\Infrastructure\DefinitionValidator;
10
use Yiisoft\Di\Contracts\ServiceProviderInterface;
11
use Yiisoft\Definitions\ArrayDefinition;
12
use Yiisoft\Definitions\Exception\CircularReferenceException;
13
use Yiisoft\Definitions\Exception\InvalidConfigException;
14
use Yiisoft\Definitions\Exception\NotFoundException;
15
use Yiisoft\Definitions\Exception\NotInstantiableException;
16
17
use function array_key_exists;
18
use function array_keys;
19
use function get_class;
20
use function implode;
21
use function in_array;
22
use function is_array;
23
use function is_object;
24
use function is_string;
25
26
/**
27
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
28
 */
29
final class Container implements ContainerInterface
30
{
31
    private const META_TAGS = 'tags';
32
    private const META_RESET = 'reset';
33
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET];
34
35
    /**
36
     * @var DefinitionStorage storage of object definitions
37
     */
38
    private DefinitionStorage $definitions;
39
    /**
40
     * @var array used to collect ids instantiated during build
41
     * to detect circular references
42
     */
43
    private array $building = [];
44
45
    /**
46
     * @var bool $validate Validate definitions when set
47
     */
48
    private bool $validate;
49
50
    /**
51
     * @var object[]
52
     */
53
    private array $instances = [];
54
55
    private CompositeContainer $delegates;
56
57
    private array $tags;
58
59
    private array $resetters = [];
60
    /** @psalm-suppress PropertyNotSetInConstructor */
61
    private DependencyResolver $dependencyResolver;
62
63
    /**
64
     * Container constructor.
65
     *
66
     * @param array $definitions Definitions to put into container.
67
     * @param array $providers Service providers to get definitions from.
68
     * lookup to when resolving dependencies. If provided the current container
69
     * is no longer queried for dependencies.
70
     * @param array $tags
71
     * @param bool $validate If definitions should be validated immediately.
72
     * @param array $delegates Container delegates. Each delegate must is a callable in format
73
     * "function (ContainerInterface $container): ContainerInterface". The container instance returned is used
74
     * in case a service can not be found in primary container.
75
     *
76
     * @throws InvalidConfigException
77
     *
78
     * @psalm-suppress PropertyNotSetInConstructor
79
     */
80 94
    public function __construct(
81
        array $definitions = [],
82
        array $providers = [],
83
        array $tags = [],
84
        bool $validate = true,
85
        array $delegates = []
86
    ) {
87 94
        $this->tags = $tags;
88 94
        $this->validate = $validate;
89 94
        $this->definitions = new DefinitionStorage();
90 94
        $this->setDefaultDefinitions();
91 94
        $this->setMultiple($definitions);
92 88
        $this->addProviders($providers);
93 87
        $this->dependencyResolver = new DependencyResolver($this);
94 87
        $this->dependencyResolver = new DependencyResolver($this->get(ContainerInterface::class));
95 87
        $this->setDelegates($delegates);
96 87
    }
97
98
    /**
99
     * Returns a value indicating whether the container has the definition of the specified name.
100
     *
101
     * @param string $id class name, interface name or alias name
102
     *
103
     * @return bool whether the container is able to provide instance of class specified.
104
     *
105
     * @see set()
106
     */
107 26
    public function has($id): bool
108
    {
109
        /** @psalm-suppress  DocblockTypeContradiction */
110 26
        if (!is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
111
            return false;
112
        }
113
114 26
        if ($this->isTagAlias($id)) {
115 2
            $tag = substr($id, 4);
116 2
            return isset($this->tags[$tag]);
117
        }
118
119
        try {
120 24
            return $this->definitions->has($id);
121 2
        } catch (CircularReferenceException $e) {
122 2
            return true;
123
        }
124
    }
125
126
    /**
127
     * Returns an instance by either interface name or alias.
128
     *
129
     * Same instance of the class will be returned each time this method is called.
130
     *
131
     * @param string $id The interface or an alias name that was previously registered.
132
     *
133
     * @throws CircularReferenceException
134
     * @throws InvalidConfigException
135
     * @throws NotFoundException
136
     * @throws NotInstantiableException
137
     *
138
     * @return mixed|object An instance of the requested interface.
139
     *
140
     * @psalm-template T
141
     * @psalm-param string|class-string<T> $id
142
     * @psalm-return ($id is class-string ? T : mixed)
143
     */
144 87
    public function get($id)
145
    {
146
        /** @psalm-suppress TypeDoesNotContainType */
147 87
        if (!is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
148
            throw new \InvalidArgumentException("Id must be a string, {$this->getVariableType($id)} given.");
149
        }
150
151 87
        if (!array_key_exists($id, $this->instances)) {
152
            try {
153 87
                $this->instances[$id] = $this->build($id);
154 14
            } catch (NotFoundException $e) {
155 5
                if (!$this->delegates->has($id)) {
156 4
                    throw $e;
157
                }
158
159 1
                return $this->delegates->get($id);
160
            }
161
        }
162
163 87
        if ($id === StateResetter::class && $this->definitions->get($id) === StateResetter::class) {
164 4
            $resetters = [];
165 4
            foreach ($this->resetters as $serviceId => $callback) {
166 4
                if (isset($this->instances[$serviceId])) {
167 4
                    $resetters[$serviceId] = $callback;
168
                }
169
            }
170 4
            if ($this->delegates->has(StateResetter::class)) {
171
                $resetters[] = $this->delegates->get(StateResetter::class);
172
            }
173 4
            $this->instances[$id]->setResetters($resetters);
174
        }
175
176 87
        return $this->instances[$id];
177
    }
178
179
    /**
180
     * Sets a definition to the container. Definition may be defined multiple ways.
181
     *
182
     * @param string $id
183
     * @param mixed $definition
184
     *
185
     * @throws InvalidConfigException
186
     *
187
     * @see `DefinitionNormalizer::normalize()`
188
     */
189 94
    private function set(string $id, $definition): void
190
    {
191 94
        [$definition, $meta] = DefinitionParser::parse($definition);
192 94
        if ($this->validate) {
193 94
            $this->validateDefinition($definition, $id);
194 94
            $this->validateMeta($meta);
195
        }
196
197 94
        if (isset($meta[self::META_TAGS])) {
198 8
            if ($this->validate) {
199 8
                $this->validateTags($meta[self::META_TAGS]);
200
            }
201 8
            $this->setTags($id, $meta[self::META_TAGS]);
202
        }
203 94
        if (isset($meta[self::META_RESET])) {
204 5
            $this->setResetter($id, $meta[self::META_RESET]);
205
        }
206
207 94
        unset($this->instances[$id]);
208 94
        $this->definitions->set($id, $definition);
209 94
    }
210
211
    /**
212
     * Sets multiple definitions at once.
213
     *
214
     * @param array $config definitions indexed by their ids
215
     *
216
     * @throws InvalidConfigException
217
     */
218 94
    private function setMultiple(array $config): void
219
    {
220 94
        foreach ($config as $id => $definition) {
221 94
            if ($this->validate && !is_string($id)) {
222 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
223
            }
224 94
            $this->set($id, $definition);
225
        }
226 94
    }
227
228 94
    private function setDefaultDefinitions(): void
229
    {
230 94
        $this->setMultiple([
231 94
            ContainerInterface::class => $this,
232
            StateResetter::class => StateResetter::class,
233
        ]);
234 94
    }
235
236
    /**
237
     * Set container delegates.
238
     *
239
     * Each delegate must is a callable in format "function (ContainerInterface $container): ContainerInterface".
240
     * The container instance returned is used in case a service can not be found in primary container.
241
     *
242
     * @param array $delegates
243
     *
244
     * @throws InvalidConfigException
245
     */
246 87
    private function setDelegates(array $delegates): void
247
    {
248 87
        $this->delegates = new CompositeContainer();
249 87
        foreach ($delegates as $delegate) {
250 1
            if (!$delegate instanceof Closure) {
251
                throw new InvalidConfigException(
252
                    'Delegate must be callable in format "function (ContainerInterface $container): ContainerInterface".'
253
                );
254
            }
255
256 1
            $delegate = $delegate($this);
257
258 1
            if (!$delegate instanceof ContainerInterface) {
259
                throw new InvalidConfigException(
260
                    'Delegate callable must return an object that implements ContainerInterface.'
261
                );
262
            }
263
264 1
            $this->delegates->attach($delegate);
265
        }
266 87
        $this->definitions->setDelegateContainer($this->delegates);
267 87
    }
268
269
    /**
270
     * @param mixed $definition
271
     *
272
     * @throws InvalidConfigException
273
     */
274 94
    private function validateDefinition($definition, ?string $id = null): void
275
    {
276 94
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
277 36
            $class = $definition['class'];
278 36
            $constructorArguments = $definition['__construct()'];
279 36
            $methodsAndProperties = $definition['methodsAndProperties'];
280 36
            $definition = array_merge(
281 36
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
282 36
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
283
                $methodsAndProperties,
284
            );
285
        }
286
287 94
        if ($definition instanceof ExtensibleService) {
288
            throw new InvalidConfigException('Invalid definition. ExtensibleService is only allowed in provider extensions.');
289
        }
290
291 94
        DefinitionValidator::validate($definition, $id);
292 94
    }
293
294
    /**
295
     * @throws InvalidConfigException
296
     */
297 94
    private function validateMeta(array $meta): void
298
    {
299 94
        foreach ($meta as $key => $_value) {
300 16
            if (!in_array($key, self::ALLOWED_META, true)) {
301 3
                throw new InvalidConfigException(
302 3
                    sprintf(
303 3
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
304
                        $key,
305
                        $key,
306
                        $key,
307
                    )
308
                );
309
            }
310
        }
311 94
    }
312
313 8
    private function validateTags(array $tags): void
314
    {
315 8
        foreach ($tags as $tag) {
316 8
            if (!is_string($tag)) {
317
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
318
            }
319
        }
320 8
    }
321
322 8
    private function setTags(string $id, array $tags): void
323
    {
324 8
        foreach ($tags as $tag) {
325 8
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
326 8
                $this->tags[$tag][] = $id;
327
            }
328
        }
329 8
    }
330
331 4
    private function setResetter(string $id, Closure $resetter): void
332
    {
333 4
        $this->resetters[$id] = $resetter;
334 4
    }
335
336
    /**
337
     * Creates new instance by either interface name or alias.
338
     *
339
     * @param string $id The interface or an alias name that was previously registered.
340
     *
341
     * @throws CircularReferenceException
342
     * @throws InvalidConfigException
343
     * @throws NotFoundException
344
     *
345
     * @return mixed|object New built instance of the specified class.
346
     *
347
     * @internal
348
     */
349 87
    private function build(string $id)
350
    {
351 87
        if ($this->isTagAlias($id)) {
352 9
            return $this->getTaggedServices($id);
353
        }
354
355 87
        if (isset($this->building[$id])) {
356 5
            if ($id === ContainerInterface::class) {
357 1
                return $this;
358
            }
359 4
            throw new CircularReferenceException(sprintf(
360 4
                'Circular reference to "%s" detected while building: %s.',
361
                $id,
362 4
                implode(', ', array_keys($this->building))
363
            ));
364
        }
365
366 87
        $this->building[$id] = 1;
367
        try {
368 87
            $object = $this->buildInternal($id);
369 87
        } finally {
370 87
            unset($this->building[$id]);
371
        }
372
373 87
        return $object;
374
    }
375
376 87
    private function isTagAlias(string $id): bool
377
    {
378 87
        return strpos($id, 'tag@') === 0;
379
    }
380
381 9
    private function getTaggedServices(string $tagAlias): array
382
    {
383 9
        $tag = substr($tagAlias, 4);
384 9
        $services = [];
385 9
        if (isset($this->tags[$tag])) {
386 8
            foreach ($this->tags[$tag] as $service) {
387 8
                $services[] = $this->get($service);
388
            }
389
        }
390
391 9
        return $services;
392
    }
393
394
    /**
395
     * @param string $id
396
     *
397
     * @throws InvalidConfigException
398
     * @throws NotFoundException
399
     *
400
     * @return mixed|object
401
     */
402 87
    private function buildInternal(string $id)
403
    {
404 87
        if ($this->definitions->has($id)) {
405 87
            $definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id);
406
407 87
            return $definition->resolve($this->dependencyResolver);
408
        }
409
410 5
        throw new NotFoundException($id, $this->definitions->getBuildStack());
411
    }
412
413 88
    private function addProviders(array $providers): void
414
    {
415 88
        $extensions = [];
416 88
        foreach ($providers as $provider) {
417 5
            $providerInstance = $this->buildProvider($provider);
418 5
            $extensions[] = $providerInstance->getExtensions();
419 5
            $this->addProviderDefinitions($providerInstance);
420
        }
421
422 88
        foreach ($extensions as $providerExtensions) {
423 5
            foreach ($providerExtensions as $id => $extension) {
424 4
                if (!$this->definitions->has($id)) {
425 1
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
426
                }
427
428 4
                $definition = $this->definitions->get($id);
429 4
                if (!$definition instanceof ExtensibleService) {
430 4
                    $definition = new ExtensibleService($definition);
431 4
                    $this->definitions->set($id, $definition);
432
                }
433
434 4
                $definition->addExtension($extension);
435
            }
436
        }
437 87
    }
438
439
    /**
440
     * Adds service provider definitions to the container.
441
     *
442
     * @param object $provider
443
     *
444
     * @throws InvalidConfigException
445
     * @throws NotInstantiableException
446
     *
447
     * @see ServiceProviderInterface
448
     */
449 5
    private function addProviderDefinitions($provider): void
450
    {
451 5
        $definitions = $provider->getDefinitions();
452 5
        $this->setMultiple($definitions);
453 5
    }
454
455
    /**
456
     * Builds service provider by definition.
457
     *
458
     * @param mixed $provider Class name or instance of provider.
459
     *
460
     * @throws InvalidConfigException If provider argument is not valid.
461
     *
462
     * @return ServiceProviderInterface Instance of service provider.
463
     *
464
     * @psalm-suppress MoreSpecificReturnType
465
     */
466 5
    private function buildProvider($provider): ServiceProviderInterface
467
    {
468 5
        if ($this->validate && !(is_string($provider) || is_object($provider) && $provider instanceof ServiceProviderInterface)) {
469
            throw new InvalidConfigException(
470
                sprintf(
471
                    'Service provider should be a class name or an instance of %s. %s given.',
472
                    ServiceProviderInterface::class,
473
                    $this->getVariableType($provider)
474
                )
475
            );
476
        }
477
478 5
        $providerInstance = is_object($provider) ? $provider : new $provider();
479 5
        if (!$providerInstance instanceof ServiceProviderInterface) {
480
            throw new InvalidConfigException(
481
                sprintf(
482
                    'Service provider should be an instance of %s. %s given.',
483
                    ServiceProviderInterface::class,
484
                    $this->getVariableType($providerInstance)
485
                )
486
            );
487
        }
488
489
        /**
490
         * @psalm-suppress LessSpecificReturnStatement
491
         */
492 5
        return $providerInstance;
493
    }
494
495
    /**
496
     * @param mixed $variable
497
     */
498 1
    private function getVariableType($variable): string
499
    {
500 1
        return is_object($variable) ? get_class($variable) : gettype($variable);
501
    }
502
}
503