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