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