Test Failed
Pull Request — master (#232)
by Dmitriy
13:01
created

Container::getVariableType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 6
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Di;
6
7
use Closure;
8
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
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
use Yiisoft\Di\Helpers\DefinitionNormalizer;
17
use Yiisoft\Di\Helpers\DefinitionParser;
18
use Yiisoft\Di\Helpers\TagHelper;
19
20
use function array_key_exists;
21
use function array_keys;
22
use function implode;
23
use function in_array;
24
use function is_array;
25
use function is_callable;
26
use function is_object;
27
use function is_string;
28
29
/**
30
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
31
 */
32
final class Container implements ContainerInterface
33
{
34
    private const META_TAGS = 'tags';
35
    private const META_RESET = 'reset';
36
    private const META_LAZY = 'lazy';
37
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET, self::META_LAZY];
38
39
    /**
40
     * @var DefinitionStorage Storage of object definitions.
41
     */
42
    private DefinitionStorage $definitions;
43
44
    /**
45
     * @var array Used to collect IDs of objects instantiated during build
46
     * to detect circular references.
47
     */
48
    private array $building = [];
49
50
    /**
51
     * @var bool $validate If definitions should be validated.
52
     */
53
    private bool $validate;
54
55
    private array $instances = [];
56
57
    private CompositeContainer $delegates;
58
59
    /**
60
     * @var array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`.
61
     * @psalm-var array<string, list<string>>
62
     */
63
    private array $tags;
64
65
    /**
66
     * @var Closure[]
67
     */
68
    private array $resetters = [];
69
    private bool $useResettersFromMeta = true;
70
    private LazyLoadingValueHolderFactory $lazyFactory;
71
72
    /**
73
     * @param ContainerConfigInterface $config Container configuration.
74 134
     *
75
     * @throws InvalidConfigException If configuration is not valid.
76 134
     */
77
    public function __construct(ContainerConfigInterface $config)
78 134
    {
79
        $this->definitions = new DefinitionStorage(
80
            [
81 134
                ContainerInterface::class => $this,
82
                StateResetter::class => StateResetter::class,
83 134
            ],
84 134
            $config->useStrictMode()
85 131
        );
86 122
        $this->validate = $config->shouldValidate();
87 116
        $this->setTags($config->getTags());
88
        $this->addDefinitions($config->getDefinitions());
89
        $this->addProviders($config->getProviders());
90
        $this->setDelegates($config->getDelegates());
91
    }
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 47
     *
100
     * @see addDefinition()
101 47
     */
102 3
    public function has(string $id): bool
103 3
    {
104
        if (TagHelper::isTagAlias($id)) {
105
            $tag = TagHelper::extractTagFromAlias($id);
106
            return isset($this->tags[$tag]);
107 44
        }
108 3
109 3
        try {
110
            return $this->definitions->has($id);
111
        } catch (CircularReferenceException) {
112
            return true;
113
        }
114
    }
115
116
    /**
117
     * Returns an instance by either interface name or alias.
118
     *
119
     * Same instance of the class will be returned each time this method is called.
120
     *
121
     * @param string $id The interface or an alias name that was previously registered.
122
     *
123
     * @throws CircularReferenceException
124
     * @throws InvalidConfigException
125
     * @throws NotFoundException
126
     * @throws NotInstantiableException
127
     *
128
     * @return mixed|object An instance of the requested interface.
129
     *
130
     * @psalm-template T
131 99
     * @psalm-param string|class-string<T> $id
132
     * @psalm-return ($id is class-string ? T : mixed)
133 99
     */
134
    public function get(string $id)
135 99
    {
136 20
        if (!array_key_exists($id, $this->instances)) {
137 11
            try {
138 8
                $this->instances[$id] = $this->build($id);
139
            } catch (NotFoundException $e) {
140
                if (!$this->delegates->has($id)) {
141
                    throw $e;
142 3
                }
143
144
                /** @psalm-suppress MixedReturnStatement */
145
                return $this->delegates->get($id);
146 90
            }
147 10
        }
148 10
149 2
        if ($id === StateResetter::class) {
150
            $delegatesResetter = null;
151
            if ($this->delegates->has(StateResetter::class)) {
152
                $delegatesResetter = $this->delegates->get(StateResetter::class);
153 10
            }
154
155 10
            /** @var StateResetter $mainResetter */
156
            $mainResetter = $this->instances[$id];
157 7
158 7
            if ($this->useResettersFromMeta) {
159 7
                /** @var StateResetter[] $resetters */
160 7
                $resetters = [];
161
                foreach ($this->resetters as $serviceId => $callback) {
162
                    if (isset($this->instances[$serviceId])) {
163 7
                        $resetters[$serviceId] = $callback;
164 1
                    }
165
                }
166 7
                if ($delegatesResetter !== null) {
167 5
                    $resetters[] = $delegatesResetter;
168 1
                }
169 1
                $mainResetter->setResetters($resetters);
170
            } elseif ($delegatesResetter !== null) {
171 1
                $resetter = new StateResetter($this->get(ContainerInterface::class));
172
                $resetter->setResetters([$mainResetter, $delegatesResetter]);
173
174
                return $resetter;
175
            }
176 90
        }
177
178
        /** @psalm-suppress MixedReturnStatement */
179
        return $this->instances[$id];
180
    }
181
182
    /**
183
     * Sets a definition to the container. Definition may be defined multiple ways.
184
     *
185
     * @param string $id ID to set definition for.
186
     * @param mixed $definition Definition to set.
187
     *
188
     * @throws InvalidConfigException
189 106
     *
190
     * @see DefinitionNormalizer::normalize()
191
     */
192 106
    private function addDefinition(string $id, mixed $definition): void
193 106
    {
194 106
        /** @var mixed $definition */
195 104
        [$definition, $meta] = DefinitionParser::parse($definition);
196
        if ($this->validate) {
197
            $this->validateDefinition($definition, $id);
198
            $this->validateMeta($meta);
199
        }
200
        /**
201 98
         * @psalm-var array{reset?:Closure,tags?:string[]} $meta
202 9
         */
203
204 98
        if (isset($meta[self::META_TAGS])) {
205 7
            $this->setDefinitionTags($id, $meta[self::META_TAGS]);
206
        }
207
        if (isset($meta[self::META_RESET])) {
208 98
            $this->setDefinitionResetter($id, $meta[self::META_RESET]);
209 98
        }
210
        if (isset($meta[self::META_LAZY]) && $meta[self::META_LAZY] === true) {
211
            $definition = $this->decorateLazy($id, $definition);
212
        }
213
214
        unset($this->instances[$id]);
215
        $this->addDefinitionToStorage($id, $definition);
216
    }
217
218
    /**
219 131
     * Sets multiple definitions at once.
220
     *
221
     * @param array $config Definitions indexed by their IDs.
222 131
     *
223 107
     * @throws InvalidConfigException
224 1
     */
225 1
    private function addDefinitions(array $config): void
226
    {
227 1
        /** @var mixed $definition */
228
        foreach ($config as $id => $definition) {
229
            if ($this->validate && !is_string($id)) {
230
                throw new InvalidConfigException(
231
                    sprintf(
232
                        'Key must be a string. %s given.',
233 106
                        get_debug_type($id)
234
                    )
235
                );
236
            }
237
            /** @var string $id */
238
239
            $this->addDefinition($id, $definition);
240
        }
241
    }
242
243
    /**
244
     * Set container delegates.
245
     *
246
     * Each delegate must is a callable in format "function (ContainerInterface $container): ContainerInterface".
247 116
     * The container instance returned is used in case a service can not be found in primary container.
248
     *
249 116
     * @param array $delegates
250 116
     *
251 5
     * @throws InvalidConfigException
252 1
     */
253
    private function setDelegates(array $delegates): void
254
    {
255
        $this->delegates = new CompositeContainer();
256
        foreach ($delegates as $delegate) {
257
            if (!$delegate instanceof Closure) {
258 4
                throw new InvalidConfigException(
259
                    'Delegate must be callable in format "function (ContainerInterface $container): ContainerInterface".'
260 4
                );
261 1
            }
262
263
            /** @var ContainerInterface */
264
            $delegate = $delegate($this);
265
266 3
            if (!$delegate instanceof ContainerInterface) {
267
                throw new InvalidConfigException(
268 114
                    'Delegate callable must return an object that implements ContainerInterface.'
269
                );
270
            }
271
272
            $this->delegates->attach($delegate);
273
        }
274
        $this->definitions->setDelegateContainer($this->delegates);
275
    }
276
277 106
    /**
278
     * @param mixed $definition Definition to validate.
279 106
     * @param string|null $id ID of the definition to validate.
280
     *
281 46
     * @throws InvalidConfigException
282
     */
283
    private function validateDefinition(mixed $definition, ?string $id = null): void
284 46
    {
285
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
286
            /** @var mixed $class */
287
            $class = $definition['class'];
288
289
            /** @var mixed $constructorArguments */
290
            $constructorArguments = $definition['__construct()'];
291 46
292
            /**
293 46
             * @var array $methodsAndProperties Is always array for prepared array definition data.
294 46
             *
295 46
             * @see DefinitionParser::parse()
296
             */
297
            $methodsAndProperties = $definition['methodsAndProperties'];
298
299
            $definition = array_merge(
300 106
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
301 1
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
302
                $methodsAndProperties,
303
            );
304
        }
305
306 105
        if ($definition instanceof ExtensibleService) {
307
            throw new InvalidConfigException(
308
                'Invalid definition. ExtensibleService is only allowed in provider extensions.'
309
            );
310
        }
311
312 104
        DefinitionValidator::validate($definition, $id);
313
    }
314
315 104
    /**
316 22
     * @throws InvalidConfigException
317 3
     */
318 3
    private function validateMeta(array $meta): void
319
    {
320
        /** @var mixed $value */
321
        foreach ($meta as $key => $value) {
322
            if (!in_array($key, self::ALLOWED_META, true)) {
323
                throw new InvalidConfigException(
324
                    sprintf(
325
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
326
                        $key,
327 20
                        $key,
328 12
                        $key,
329
                    )
330
                );
331 18
            }
332 8
333
            if ($key === self::META_TAGS) {
334
                $this->validateDefinitionTags($value);
335
            }
336
337
            if ($key === self::META_RESET) {
338
                $this->validateDefinitionReset($value);
339
            }
340 12
        }
341
    }
342 12
343 1
    /**
344 1
     * @throws InvalidConfigException
345
     */
346 1
    private function validateDefinitionTags(mixed $tags): void
347
    {
348
        if (!is_array($tags)) {
349
            throw new InvalidConfigException(
350
                sprintf(
351 11
                    'Invalid definition: tags should be array of strings, %s given.',
352 11
                    get_debug_type($tags)
353 1
                )
354
            );
355
        }
356
357
        foreach ($tags as $tag) {
358
            if (!is_string($tag)) {
359
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
360
            }
361 8
        }
362
    }
363 8
364 1
    /**
365 1
     * @throws InvalidConfigException
366
     */
367 1
    private function validateDefinitionReset(mixed $reset): void
368
    {
369
        if (!$reset instanceof Closure) {
370
            throw new InvalidConfigException(
371
                sprintf(
372
                    'Invalid definition: "reset" should be closure, %s given.',
373
                    get_debug_type($reset)
374
                )
375
            );
376 134
        }
377
    }
378 134
379 134
    /**
380 5
     * @throws InvalidConfigException
381 1
     */
382 1
    private function setTags(array $tags): void
383
    {
384
        if ($this->validate) {
385
            foreach ($tags as $tag => $services) {
386
                if (!is_string($tag)) {
387
                    throw new InvalidConfigException(
388 4
                        sprintf(
389 1
                            'Invalid tags configuration: tag should be string, %s given.',
390 1
                            $tag
391
                        )
392 1
                    );
393
                }
394
                if (!is_array($services)) {
395
                    throw new InvalidConfigException(
396
                        sprintf(
397 3
                            'Invalid tags configuration: tag should contain array of service IDs, %s given.',
398 3
                            get_debug_type($services)
399 1
                        )
400 1
                    );
401
                }
402 1
                /** @var mixed $service */
403
                foreach ($services as $service) {
404
                    if (!is_string($service)) {
405
                        throw new InvalidConfigException(
406
                            sprintf(
407
                                'Invalid tags configuration: service should be defined as class string, %s given.',
408
                                get_debug_type($service)
409
                            )
410
                        );
411 131
                    }
412
                }
413
            }
414
        }
415
        /** @psalm-var array<string, list<string>> $tags */
416
417 9
        $this->tags = $tags;
418
    }
419 9
420 9
    /**
421 9
     * @psalm-param string[] $tags
422
     */
423
    private function setDefinitionTags(string $id, array $tags): void
424
    {
425
        foreach ($tags as $tag) {
426 7
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
427
                $this->tags[$tag][] = $id;
428 7
            }
429
        }
430
    }
431
432
    private function setDefinitionResetter(string $id, Closure $resetter): void
433
    {
434
        $this->resetters[$id] = $resetter;
435
    }
436
437
    /**
438
     * Add definition to storage.
439 98
     *
440
     * @see $definitions
441 98
     *
442
     * @param string $id ID to set definition for.
443 98
     * @param mixed|object $definition Definition to set.
444 5
     */
445
    private function addDefinitionToStorage(string $id, $definition): void
446
    {
447
        $this->definitions->set($id, $definition);
448
449
        if ($id === StateResetter::class) {
450
            $this->useResettersFromMeta = false;
451
        }
452
    }
453
454
    /**
455
     * Creates new instance by either interface name or alias.
456
     *
457
     * @param string $id The interface or an alias name that was previously registered.
458
     *
459
     * @throws CircularReferenceException
460
     * @throws InvalidConfigException
461 99
     * @throws NotFoundException
462
     *
463 99
     * @return mixed|object New built instance of the specified class.
464 10
     *
465
     * @internal
466
     */
467 98
    private function build(string $id)
468 89
    {
469 89
        if (TagHelper::isTagAlias($id)) {
470
            return $this->getTaggedServices($id);
471 4
        }
472
473
        if (isset($this->building[$id])) {
474 4
            if ($id === ContainerInterface::class) {
475
                return $this;
476
            }
477
            throw new CircularReferenceException(sprintf(
478 98
                'Circular reference to "%s" detected while building: %s.',
479
                $id,
480
                implode(', ', array_keys($this->building))
481 98
            ));
482 89
        }
483 98
484
        $this->building[$id] = 1;
485
        try {
486 89
            /** @var mixed $object */
487
            $object = $this->buildInternal($id);
488
        } finally {
489 10
            unset($this->building[$id]);
490
        }
491 10
492 10
        return $object;
493 10
    }
494 9
495
    private function getTaggedServices(string $tagAlias): array
496 9
    {
497
        $tag = TagHelper::extractTagFromAlias($tagAlias);
498
        $services = [];
499
        if (isset($this->tags[$tag])) {
500 10
            foreach ($this->tags[$tag] as $service) {
501
                /** @var mixed */
502
                $services[] = $this->get($service);
503
            }
504
        }
505
506
        return $services;
507
    }
508
509 98
    /**
510
     * @throws InvalidConfigException
511 98
     * @throws NotFoundException
512 89
     *
513
     * @return mixed|object
514 89
     */
515
    private function buildInternal(string $id)
516
    {
517 11
        if ($this->definitions->has($id)) {
518
            $definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id);
519
520
            return $definition->resolve($this->get(ContainerInterface::class));
521
        }
522
523
        throw new NotFoundException($id, $this->definitions->getBuildStack());
524 122
    }
525
526 122
    /**
527
     * @throws CircularReferenceException
528 122
     * @throws InvalidConfigException
529 15
     */
530 13
    private function addProviders(array $providers): void
531 13
    {
532
        $extensions = [];
533
        /** @var mixed $provider */
534 120
        foreach ($providers as $provider) {
535
            $providerInstance = $this->buildProvider($provider);
536 13
            $extensions[] = $providerInstance->getExtensions();
537 10
            $this->addDefinitions($providerInstance->getDefinitions());
538 1
        }
539 1
540
        foreach ($extensions as $providerExtensions) {
541
            /** @var mixed $extension */
542
            foreach ($providerExtensions as $id => $extension) {
543 9
                if (!is_string($id)) {
544 1
                    throw new InvalidConfigException(
545
                        sprintf('Extension key must be a service ID as string, %s given.', $id)
546
                    );
547 8
                }
548 1
549
                if ($id === ContainerInterface::class) {
550
                    throw new InvalidConfigException('ContainerInterface extensions are not allowed.');
551 8
                }
552 1
553 1
                if (!$this->definitions->has($id)) {
554
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
555 1
                }
556
557
                if (!is_callable($extension)) {
558
                    throw new InvalidConfigException(
559
                        sprintf(
560
                            'Extension of service should be callable, %s given.',
561 7
                            get_debug_type($extension)
562 7
                        )
563 7
                    );
564 7
                }
565
566
                /** @var mixed $definition */
567 7
                $definition = $this->definitions->get($id);
568
                if (!$definition instanceof ExtensibleService) {
569
                    $definition = new ExtensibleService($definition, $id);
570
                    $this->addDefinitionToStorage($id, $definition);
571
                }
572
573
                $definition->addExtension($extension);
574
            }
575
        }
576
    }
577
578
    /**
579
     * Builds service provider by definition.
580
     *
581 15
     * @param mixed $provider Class name or instance of provider.
582
     *
583 15
     * @throws InvalidConfigException If provider argument is not valid.
584 1
     *
585 1
     * @return ServiceProviderInterface Instance of service provider.
586
     */
587
    private function buildProvider(mixed $provider): ServiceProviderInterface
588 1
    {
589
        if ($this->validate && !(is_string($provider) || $provider instanceof ServiceProviderInterface)) {
590
            throw new InvalidConfigException(
591
                sprintf(
592
                    'Service provider should be a class name or an instance of %s. %s given.',
593
                    ServiceProviderInterface::class,
594
                    get_debug_type($provider)
595
                )
596
            );
597 14
        }
598 14
599 1
        /**
600 1
         * @psalm-suppress MixedMethodCall Service provider defined as class string
601
         * should container public constructor, otherwise throws error.
602
         */
603 1
        $providerInstance = is_object($provider) ? $provider : new $provider();
604
        if (!$providerInstance instanceof ServiceProviderInterface) {
605
            throw new InvalidConfigException(
606
                sprintf(
607
                    'Service provider should be an instance of %s. %s given.',
608 13
                    ServiceProviderInterface::class,
609
                    get_debug_type($providerInstance)
610
                )
611
            );
612
        }
613
614
        return $providerInstance;
615
    }
616
617
    /**
618
     * @param mixed $variable
619
     */
620
    private function getVariableType($variable): string
0 ignored issues
show
Unused Code introduced by
The method getVariableType() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
621
    {
622
        if (is_object($variable)) {
623
            return get_class($variable);
624
        }
625
626
        return gettype($variable);
627
    }
628
629
    private function decorateLazy(string $id, DefinitionInterface $definition): DefinitionInterface
0 ignored issues
show
Bug introduced by
The type Yiisoft\Di\DefinitionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
630
    {
631
        $factory = $this->getLazyLoadingValueHolderFactory();
632
        if (class_exists($id) || interface_exists($id)) {
633
            $class = $id;
634
        } elseif ($definition instanceof ArrayDefinition) {
635
            $class = $definition->getClass();
636
        } else {
637
            throw new \RuntimeException('Could not determinate object class');
638
        }
639
640
        return new LazyDefinitionDecorator($factory, $definition, $class);
0 ignored issues
show
Bug introduced by
The type Yiisoft\Di\LazyDefinitionDecorator was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
641
    }
642
643
    private function getLazyLoadingValueHolderFactory(): LazyLoadingValueHolderFactory
644
    {
645
        if (!class_exists(LazyLoadingValueHolderFactory::class)) {
646
            throw new \RuntimeException('You should install `ocramius/proxy-manager` if you want to use lazy services.');
647
        }
648
        return $this->lazyFactory ??= new LazyLoadingValueHolderFactory();
649
    }
650
}
651