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