Passed
Pull Request — master (#279)
by Dmitriy
02:34
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 99
    public function __construct(ContainerConfigInterface $config)
74
    {
75 99
        $this->definitions = new DefinitionStorage([], $config->useStrictMode());
76 99
        $this->tags = $config->getTags();
77 99
        $this->validate = $config->shouldValidate();
78 99
        $this->setDefaultDefinitions();
79 99
        $this->setMultiple($config->getDefinitions());
80 93
        $this->addProviders($config->getProviders());
81 91
        $this->setDelegates($config->getDelegates());
82 91
    }
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 29
    public function has($id): bool
94
    {
95
        /** @psalm-suppress  DocblockTypeContradiction */
96 29
        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 29
        if ($this->isTagAlias($id)) {
101 2
            $tag = substr($id, 4);
102 2
            return isset($this->tags[$tag]);
103
        }
104
105
        try {
106 27
            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 87
    public function get($id)
131
    {
132
        /** @psalm-suppress TypeDoesNotContainType */
133 87
        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 87
        if (!array_key_exists($id, $this->instances)) {
138
            try {
139 87
                $this->instances[$id] = $this->build($id);
140 17
            } catch (NotFoundException $e) {
141 8
                if (!$this->delegates->has($id)) {
142 7
                    throw $e;
143
                }
144
145 1
                return $this->delegates->get($id);
146
            }
147
        }
148
149 78
        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 78
        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 99
    private function set(string $id, $definition): void
176
    {
177 99
        [$definition, $meta] = DefinitionParser::parse($definition);
178 99
        if ($this->validate) {
179 99
            $this->validateDefinition($definition, $id);
180 99
            $this->validateMeta($meta);
181
        }
182
183 99
        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 99
        if (isset($meta[self::META_RESET])) {
190 5
            $this->setResetter($id, $meta[self::META_RESET]);
191
        }
192
193 99
        unset($this->instances[$id]);
194 99
        $this->definitions->set($id, $definition);
195 99
    }
196
197
    /**
198
     * Sets multiple definitions at once.
199
     *
200
     * @param array $config Definitions indexed by their IDs.
201
     *
202
     * @throws InvalidConfigException
203
     */
204 99
    private function setMultiple(array $config): void
205
    {
206 99
        foreach ($config as $id => $definition) {
207 99
            if ($this->validate && !is_string($id)) {
208 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
209
            }
210 99
            $this->set($id, $definition);
211
        }
212 99
    }
213
214 99
    private function setDefaultDefinitions(): void
215
    {
216 99
        $this->setMultiple([
217 99
            ContainerInterface::class => $this,
218
            StateResetter::class => StateResetter::class,
219
        ]);
220 99
    }
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 91
    private function setDelegates(array $delegates): void
233
    {
234 91
        $this->delegates = new CompositeContainer();
235 91
        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 91
        $this->definitions->setDelegateContainer($this->delegates);
253 91
    }
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 99
    private function validateDefinition($definition, ?string $id = null): void
262
    {
263 99
        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 99
        if ($definition instanceof ExtensibleService) {
275
            throw new InvalidConfigException('Invalid definition. ExtensibleService is only allowed in provider extensions.');
276
        }
277
278 99
        DefinitionValidator::validate($definition, $id);
279 99
    }
280
281
    /**
282
     * @throws InvalidConfigException
283
     */
284 99
    private function validateMeta(array $meta): void
285
    {
286 99
        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 99
    }
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 87
    private function build(string $id)
337
    {
338 87
        if ($this->isTagAlias($id)) {
339 9
            return $this->getTaggedServices($id);
340
        }
341
342 86
        if (isset($this->building[$id])) {
343 77
            if ($id === ContainerInterface::class) {
344 77
                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 86
        $this->building[$id] = 1;
354
        try {
355 86
            $object = $this->buildInternal($id);
356 77
        } finally {
357 86
            unset($this->building[$id]);
358
        }
359
360 77
        return $object;
361
    }
362
363 91
    private function isTagAlias(string $id): bool
364
    {
365 91
        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 86
    private function buildInternal(string $id)
390
    {
391 86
        if ($this->definitions->has($id)) {
392 77
            $definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id);
393
394 77
            return $definition->resolve($this->get(ContainerInterface::class));
395
        }
396
397 8
        throw new NotFoundException($id, $this->definitions->getBuildStack());
398
    }
399
400 93
    private function addProviders(array $providers): void
401
    {
402 93
        $extensions = [];
403 93
        foreach ($providers as $provider) {
404 7
            $providerInstance = $this->buildProvider($provider);
405 7
            $extensions[] = $providerInstance->getExtensions();
406 7
            $this->addProviderDefinitions($providerInstance);
407
        }
408
409 93
        foreach ($extensions as $providerExtensions) {
410 7
            foreach ($providerExtensions as $id => $extension) {
411 6
                if ($id === ContainerInterface::class) {
412 1
                    throw new InvalidConfigException("ContainerInterface extensions are not allowed.");
413
                }
414
415 5
                if (!$this->definitions->has($id)) {
416 1
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
417
                }
418
419 5
                $definition = $this->definitions->get($id);
420 5
                if (!$definition instanceof ExtensibleService) {
421 5
                    $definition = new ExtensibleService($definition);
422 5
                    $this->definitions->set($id, $definition);
423
                }
424
425 5
                $definition->addExtension($extension);
426
            }
427
        }
428 91
    }
429
430
    /**
431
     * Adds service provider definitions to the container.
432
     *
433
     * @param ServiceProviderInterface $provider Provider to get definitions from.
434
     *
435
     * @throws InvalidConfigException
436
     * @throws NotInstantiableException
437
     */
438 7
    private function addProviderDefinitions(ServiceProviderInterface $provider): void
439
    {
440 7
        $definitions = $provider->getDefinitions();
441 7
        $this->setMultiple($definitions);
442 7
    }
443
444
    /**
445
     * Builds service provider by definition.
446
     *
447
     * @param mixed $provider Class name or instance of provider.
448
     *
449
     * @throws InvalidConfigException If provider argument is not valid.
450
     *
451
     * @return ServiceProviderInterface Instance of service provider.
452
     *
453
     * @psalm-suppress MoreSpecificReturnType
454
     */
455 7
    private function buildProvider($provider): ServiceProviderInterface
456
    {
457 7
        if ($this->validate && !(is_string($provider) || $provider instanceof ServiceProviderInterface)) {
458
            throw new InvalidConfigException(
459
                sprintf(
460
                    'Service provider should be a class name or an instance of %s. %s given.',
461
                    ServiceProviderInterface::class,
462
                    $this->getVariableType($provider)
463
                )
464
            );
465
        }
466
467 7
        $providerInstance = is_object($provider) ? $provider : new $provider();
468 7
        if (!$providerInstance instanceof ServiceProviderInterface) {
469
            throw new InvalidConfigException(
470
                sprintf(
471
                    'Service provider should be an instance of %s. %s given.',
472
                    ServiceProviderInterface::class,
473
                    $this->getVariableType($providerInstance)
474
                )
475
            );
476
        }
477
478
        /**
479
         * @psalm-suppress LessSpecificReturnStatement
480
         */
481 7
        return $providerInstance;
482
    }
483
484
    /**
485
     * @param mixed $variable
486
     */
487 1
    private function getVariableType($variable): string
488
    {
489 1
        return is_object($variable) ? get_class($variable) : gettype($variable);
490
    }
491
}
492