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