Passed
Pull Request — master (#250)
by Dmitriy
04:39 queued 01:52
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\Di\Contracts\ServiceProviderInterface;
10
use Yiisoft\Factory\Definition\ArrayDefinition;
11
use Yiisoft\Factory\Definition\DefinitionValidator;
12
use Yiisoft\Factory\Exception\CircularReferenceException;
13
use Yiisoft\Factory\Exception\InvalidConfigException;
14
use Yiisoft\Factory\Exception\NotFoundException;
15
use Yiisoft\Factory\Exception\NotInstantiableException;
16
17
use function array_key_exists;
18
use function array_keys;
19
use function get_class;
20
use function implode;
21
use function in_array;
22
use function is_array;
23
use function is_object;
24
use function is_string;
25
26
/**
27
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
28
 */
29
final class Container implements ContainerInterface
30
{
31
    private const META_TAGS = 'tags';
32
    private const META_RESET = 'reset';
33
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET];
34
35
    /**
36
     * @var DefinitionStorage storage of object definitions
37
     */
38
    private DefinitionStorage $definitions;
39
    /**
40
     * @var array used to collect ids instantiated during build
41
     * to detect circular references
42
     */
43
    private array $building = [];
44
45
    /**
46
     * @var bool $validate Validate definitions when set
47
     */
48
    private bool $validate;
49
50
    /**
51
     * @var object[]
52
     */
53
    private array $instances = [];
54
55
    private CompositeContainer $delegates;
56
57
    private array $tags;
58
59
    private array $resetters = [];
60
    /** @psalm-suppress PropertyNotSetInConstructor */
61
    private DependencyResolver $dependencyResolver;
62
63
    /**
64
     * Container constructor.
65
     *
66
     * @param array $definitions Definitions to put into container.
67
     * @param array $providers Service providers to get definitions from.
68
     * lookup to when resolving dependencies. If provided the current container
69
     * is no longer queried for dependencies.
70
     *
71
     * @throws InvalidConfigException
72
     *
73
     * @psalm-suppress PropertyNotSetInConstructor
74
     */
75 94
    public function __construct(
76
        array $definitions = [],
77
        array $providers = [],
78
        array $tags = [],
79
        bool $validate = true,
80
        array $delegates = []
81
    ) {
82 94
        $this->tags = $tags;
83 94
        $this->validate = $validate;
84 94
        $this->definitions = new DefinitionStorage();
85 94
        $this->setDefaultDefinitions();
86 94
        $this->setMultiple($definitions);
87 88
        $this->addProviders($providers);
88 87
        $this->dependencyResolver = new DependencyResolver($this);
89 87
        $this->dependencyResolver = new DependencyResolver($this->get(ContainerInterface::class));
90 87
        $this->setDelegates($delegates);
91 87
    }
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 87
    public function get($id)
140
    {
141
        /** @psalm-suppress TypeDoesNotContainType */
142 87
        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 87
        if (!array_key_exists($id, $this->instances)) {
147
            try {
148 87
                $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 87
        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 87
        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
178
     * @param mixed $definition
179
     *
180
     * @throws InvalidConfigException
181
     *
182
     * @see `DefinitionNormalizer::normalize()`
183
     */
184 94
    private function set(string $id, $definition): void
185
    {
186 94
        [$definition, $meta] = DefinitionParser::parse($definition);
187 94
        if ($this->validate) {
188 94
            $this->validateDefinition($definition, $id);
189 94
            $this->validateMeta($meta);
190
        }
191
192 94
        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 94
        if (isset($meta[self::META_RESET])) {
199 5
            $this->setResetter($id, $meta[self::META_RESET]);
200
        }
201
202 94
        unset($this->instances[$id]);
203 94
        $this->definitions->set($id, $definition);
204 94
    }
205
206
    /**
207
     * Sets multiple definitions at once.
208
     *
209
     * @param array $config definitions indexed by their ids
210
     *
211
     * @throws InvalidConfigException
212
     */
213 94
    private function setMultiple(array $config): void
214
    {
215 94
        foreach ($config as $id => $definition) {
216 94
            if ($this->validate && !is_string($id)) {
217 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
218
            }
219 94
            $this->set($id, $definition);
220
        }
221 94
    }
222
223 94
    private function setDefaultDefinitions(): void
224
    {
225 94
        $this->setMultiple([
226 94
            ContainerInterface::class => $this,
227
            StateResetter::class => StateResetter::class,
228
        ]);
229 94
    }
230
231
    /**
232
     * @param array $delegates
233
     *
234
     * @throws InvalidConfigException
235
     */
236 87
    private function setDelegates(array $delegates): void
237
    {
238 87
        $this->delegates = new CompositeContainer();
239 87
        foreach ($delegates as $delegate) {
240 1
            if (!$delegate instanceof \Closure) {
241
                throw new InvalidConfigException(
242
                    'Delegate must be callable in format "fn (ContainerInterface $conatiner) => MyContainer($container)"'
243
                );
244
            }
245
246 1
            $delegate = $delegate($this);
247
248 1
            if (!$delegate instanceof ContainerInterface) {
249
                throw new InvalidConfigException(
250
                    'Delegate callable must return an object that implements ContainerInterface'
251
                );
252
            }
253
254 1
            $this->delegates->attach($delegate);
255
        }
256 87
        $this->definitions->setDelegateContainer($this->delegates);
257 87
    }
258
259
    /**
260
     * @param mixed $definition
261
     *
262
     * @throws InvalidConfigException
263
     */
264 94
    private function validateDefinition($definition, ?string $id = null): void
265
    {
266 94
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
267 36
            $class = $definition['class'];
268 36
            $constructorArguments = $definition['__construct()'];
269 36
            $methodsAndProperties = $definition['methodsAndProperties'];
270 36
            $definition = array_merge(
271 36
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
272 36
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
273
                $methodsAndProperties,
274
            );
275
        }
276
277 94
        if ($definition instanceof ExtensibleService) {
278
            throw new InvalidConfigException('Invalid definition. ExtensibleService is only allowed in provider extensions.');
279
        }
280
281 94
        DefinitionValidator::validate($definition, $id);
282 94
    }
283
284
    /**
285
     * @throws InvalidConfigException
286
     */
287 94
    private function validateMeta(array $meta): void
288
    {
289 94
        foreach ($meta as $key => $_value) {
290 16
            if (!in_array($key, self::ALLOWED_META, true)) {
291 3
                throw new InvalidConfigException(
292 3
                    sprintf(
293 3
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
294
                        $key,
295
                        $key,
296
                        $key,
297
                    )
298
                );
299
            }
300
        }
301 94
    }
302
303 8
    private function validateTags(array $tags): void
304
    {
305 8
        foreach ($tags as $tag) {
306 8
            if (!is_string($tag)) {
307
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
308
            }
309
        }
310 8
    }
311
312 8
    private function setTags(string $id, array $tags): void
313
    {
314 8
        foreach ($tags as $tag) {
315 8
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
316 8
                $this->tags[$tag][] = $id;
317
            }
318
        }
319 8
    }
320
321 4
    private function setResetter(string $id, Closure $resetter): void
322
    {
323 4
        $this->resetters[$id] = $resetter;
324 4
    }
325
326
    /**
327
     * Creates new instance by either interface name or alias.
328
     *
329
     * @param string $id The interface or an alias name that was previously registered.
330
     *
331
     * @throws CircularReferenceException
332
     * @throws InvalidConfigException
333
     * @throws NotFoundException
334
     *
335
     * @return mixed|object New built instance of the specified class.
336
     *
337
     * @internal
338
     */
339 87
    private function build(string $id)
340
    {
341 87
        if ($this->isTagAlias($id)) {
342 9
            return $this->getTaggedServices($id);
343
        }
344
345 87
        if (isset($this->building[$id])) {
346 5
            if ($id === ContainerInterface::class) {
347 1
                return $this;
348
            }
349 4
            throw new CircularReferenceException(sprintf(
350 4
                'Circular reference to "%s" detected while building: %s.',
351
                $id,
352 4
                implode(', ', array_keys($this->building))
353
            ));
354
        }
355
356 87
        $this->building[$id] = 1;
357
        try {
358 87
            $object = $this->buildInternal($id);
359 87
        } finally {
360 87
            unset($this->building[$id]);
361
        }
362
363 87
        return $object;
364
    }
365
366 87
    private function isTagAlias(string $id): bool
367
    {
368 87
        return strpos($id, 'tag@') === 0;
369
    }
370
371 9
    private function getTaggedServices(string $tagAlias): array
372
    {
373 9
        $tag = substr($tagAlias, 4);
374 9
        $services = [];
375 9
        if (isset($this->tags[$tag])) {
376 8
            foreach ($this->tags[$tag] as $service) {
377 8
                $services[] = $this->get($service);
378
            }
379
        }
380
381 9
        return $services;
382
    }
383
384
    /**
385
     * @param string $id
386
     *
387
     * @throws InvalidConfigException
388
     * @throws NotFoundException
389
     *
390
     * @return mixed|object
391
     */
392 87
    private function buildInternal(string $id)
393
    {
394 87
        if ($this->definitions->has($id)) {
395 87
            $definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id);
396
397 87
            return $definition->resolve($this->dependencyResolver);
398
        }
399
400 5
        throw new NotFoundException($id, $this->definitions->getLastBuilding());
0 ignored issues
show
Unused Code introduced by
The call to Yiisoft\Factory\Exceptio...xception::__construct() has too many arguments starting with $this->definitions->getLastBuilding(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

400
        throw /** @scrutinizer ignore-call */ new NotFoundException($id, $this->definitions->getLastBuilding());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
401
    }
402
403 88
    private function addProviders(array $providers): void
404
    {
405 88
        $extensions = [];
406 88
        foreach ($providers as $provider) {
407 5
            $providerInstance = $this->buildProvider($provider);
408 5
            $extensions[] = $providerInstance->getExtensions();
409 5
            $this->addProviderDefinitions($providerInstance);
410
        }
411
412 88
        foreach ($extensions as $providerExtensions) {
413 5
            foreach ($providerExtensions as $id => $extension) {
414 4
                if (!$this->definitions->has($id)) {
415 1
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
416
                }
417
418 4
                $definition = $this->definitions->get($id);
419 4
                if (!$definition instanceof ExtensibleService) {
420 4
                    $definition = new ExtensibleService($definition);
421 4
                    $this->definitions->set($id, $definition);
422
                }
423
424 4
                $definition->addExtension($extension);
425
426
427
            }
428
        }
429 87
    }
430
431
    /**
432
     * Adds service provider definitions to the container.
433
     *
434
     * @param object $provider
435
     *
436
     * @throws InvalidConfigException
437
     * @throws NotInstantiableException
438
     *
439
     * @see ServiceProviderInterface
440
     */
441 5
    private function addProviderDefinitions($provider): void
442
    {
443 5
        $definitions = $provider->getDefinitions();
444 5
        $this->setMultiple($definitions);
445 5
    }
446
447
    /**
448
     * Builds service provider by definition.
449
     *
450
     * @param mixed $provider Class name or instance of provider.
451
     *
452
     * @throws InvalidConfigException If provider argument is not valid.
453
     *
454
     * @return ServiceProviderInterface Instance of service provider.
455
     *
456
     * @psalm-suppress MoreSpecificReturnType
457
     */
458 5
    private function buildProvider($provider): ServiceProviderInterface
459
    {
460 5
        if ($this->validate && !(is_string($provider) || is_object($provider) && $provider instanceof ServiceProviderInterface)) {
461
            throw new InvalidConfigException(
462
                sprintf(
463
                    'Service provider should be a class name or an instance of %s. %s given.',
464
                    ServiceProviderInterface::class,
465
                    $this->getVariableType($provider)
466
                )
467
            );
468
        }
469
470 5
        $providerInstance = is_object($provider) ? $provider : new $provider();
471 5
        if (!$providerInstance instanceof ServiceProviderInterface) {
472
            throw new InvalidConfigException(
473
                sprintf(
474
                    'Service provider should be an instance of %s. %s given.',
475
                    ServiceProviderInterface::class,
476
                    $this->getVariableType($providerInstance)
477
                )
478
            );
479
        }
480
481
        /**
482
         * @psalm-suppress LessSpecificReturnStatement
483
         */
484 5
        return $providerInstance;
485
    }
486
487
    /**
488
     * @param mixed $variable
489
     */
490 1
    private function getVariableType($variable): string
491
    {
492 1
        return is_object($variable) ? get_class($variable) : gettype($variable);
493
    }
494
}
495