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