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