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