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