Test Failed
Pull Request — master (#232)
by Dmitriy
07:51 queued 05:31
created

Container::set()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
nc 24
nop 2
dl 0
loc 24
ccs 11
cts 11
cp 1
crap 7
rs 8.8333
c 1
b 0
f 0
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\Di\Contracts\DeferredServiceProviderInterface;
11
use Yiisoft\Di\Contracts\ServiceProviderInterface;
12
use Yiisoft\Factory\Definition\ArrayDefinition;
13
use Yiisoft\Factory\Definition\Decorator\LazyDefinitionDecorator;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Factory\Definiti...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...
14
use Yiisoft\Factory\Definition\DefinitionInterface;
15
use Yiisoft\Factory\Definition\DefinitionValidator;
16
use Yiisoft\Factory\DependencyResolverInterface;
17
use Yiisoft\Factory\Exception\CircularReferenceException;
18
use Yiisoft\Factory\Exception\InvalidConfigException;
19
use Yiisoft\Factory\Exception\NotFoundException;
20
use Yiisoft\Factory\Exception\NotInstantiableException;
21
use Yiisoft\Injector\Injector;
22
use function array_key_exists;
23
use function array_keys;
24
use function assert;
25
use function class_exists;
26
use function get_class;
27
use function implode;
28
use function in_array;
29
use function is_array;
30
use function is_object;
31
use function is_string;
32
33
/**
34
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
35
 */
36
final class Container extends AbstractContainerConfigurator implements ContainerInterface
37
{
38
    private const META_TAGS = 'tags';
39
    private const META_RESET = 'reset';
40
    private const META_LAZY = 'lazy';
41
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET, self::META_LAZY];
42
43
    /**
44
     * @var array object definitions indexed by their types
45
     */
46
    private array $definitions = [];
47
    /**
48
     * @var array used to collect ids instantiated during build
49
     * to detect circular references
50
     */
51
    private array $building = [];
52
53
    /**
54
     * @var bool $validate Validate definitions when set
55
     */
56
    private bool $validate;
57
58
    /**
59
     * @var object[]
60
     */
61
    private array $instances = [];
62
63
    private array $tags;
64
65
    private array $resetters = [];
66
67
    private ?CompositeContainer $rootContainer = null;
68
    private DependencyResolverInterface $dependencyResolver;
69
    private LazyLoadingValueHolderFactory $lazyFactory;
70
71
    /**
72
     * Container constructor.
73
     *
74
     * @param array $definitions Definitions to put into container.
75
     * @param array $providers Service providers to get definitions from.
76
     * @param ContainerInterface|null $rootContainer Root container to delegate
77
     * lookup to when resolving dependencies. If provided the current container
78 98
     * is no longer queried for dependencies.
79
     *
80
     * @throws InvalidConfigException
81
     */
82
    public function __construct(
83
        array $definitions = [],
84
        array $providers = [],
85 98
        array $tags = [],
86 98
        ContainerInterface $rootContainer = null,
87 98
        bool $validate = true
88 98
    ) {
89 98
        $this->tags = $tags;
90 92
        $this->validate = $validate;
91
        $this->delegateLookup($rootContainer);
92
        $this->setDefaultDefinitions();
93 90
        $this->setMultiple($definitions);
94 90
        $this->addProviders($providers, new DependencyResolver($this));
95
96
        // Prevent circular reference to ContainerInterface
97
        $this->get(ContainerInterface::class);
98
    }
99
100
    /**
101
     * Returns a value indicating whether the container has the definition of the specified name.
102
     *
103
     * @param string $id class name, interface name or alias name
104
     *
105 31
     * @return bool whether the container is able to provide instance of class specified.
106
     *
107 31
     * @see set()
108 2
     */
109 2
    public function has($id): bool
110
    {
111
        if ($this->isTagAlias($id)) {
112 29
            $tag = substr($id, 4);
113
            return isset($this->tags[$tag]);
114
        }
115
116
        return isset($this->definitions[$id]) || class_exists($id);
117
    }
118
119
    /**
120
     * Returns an instance by either interface name or alias.
121
     *
122
     * Same instance of the class will be returned each time this method is called.
123
     *
124
     * @param string $id The interface or an alias name that was previously registered.
125
     *
126
     * @throws CircularReferenceException
127
     * @throws InvalidConfigException
128
     * @throws NotFoundException
129
     * @throws NotInstantiableException
130
     *
131
     * @return mixed|object An instance of the requested interface.
132
     *
133 91
     * @psalm-template T
134
     * @psalm-param string|class-string<T> $id
135 91
     * @psalm-return ($id is class-string ? T : mixed)
136 4
     */
137 4
    public function get($id)
138 4
    {
139 4
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
140
            $resetters = [];
141
            foreach ($this->resetters as $serviceId => $callback) {
142 4
                if (isset($this->instances[$serviceId])) {
143
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
144
                }
145 91
            }
146 91
            return new StateResetter($resetters, $this);
147
        }
148
149 91
        if (!array_key_exists($id, $this->instances)) {
150
            $this->instances[$id] = $this->build($id);
151
        }
152
153
        return $this->instances[$id];
154
    }
155
156
    /**
157 98
     * Delegate service lookup to another container.
158
     *
159 98
     * @param ContainerInterface $container
160 8
     */
161 8
    protected function delegateLookup(?ContainerInterface $container): void
162 8
    {
163
        if ($container !== null) {
164
            if ($this->rootContainer === null) {
165 8
                $this->rootContainer = new CompositeContainer();
166
                $this->setDefaultDefinitions();
167
            }
168 98
169 98
            $this->rootContainer->attach($container);
0 ignored issues
show
Bug introduced by
The method attach() does not exist on null. ( Ignorable by Annotation )

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

169
            $this->rootContainer->/** @scrutinizer ignore-call */ 
170
                                  attach($container);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
170
        }
171
172
        $this->dependencyResolver = new DependencyResolver($this->rootContainer ?? $this);
173
    }
174
175
    /**
176
     * Sets a definition to the container. Definition may be defined multiple ways.
177
     *
178
     * @param string $id
179
     * @param mixed $definition
180
     *
181 98
     * @throws InvalidConfigException
182
     *
183 98
     * @see `DefinitionNormalizer::normalize()`
184 98
     */
185 98
    protected function set(string $id, $definition): void
186 98
    {
187
        [$definition, $meta] = DefinitionParser::parse($definition);
188
        if ($this->validate) {
189 98
            $this->validateDefinition($definition, $id);
190 8
            $this->validateMeta($meta);
191 8
        }
192
        $definition = DefinitionNormalizer::normalize($definition, $id);
193 8
194
        if (isset($meta[self::META_TAGS])) {
195 98
            if ($this->validate) {
196 5
                $this->validateTags($meta[self::META_TAGS]);
197
            }
198
            $this->setTags($id, $meta[self::META_TAGS]);
199 98
        }
200 98
        if (isset($meta[self::META_RESET])) {
201 98
            $this->setResetter($id, $meta[self::META_RESET]);
202
        }
203
        if (isset($meta[self::META_LAZY]) && $meta[self::META_LAZY] === true) {
204
            $definition = $this->decorateLazy($id, $definition);
205
        }
206
207
        unset($this->instances[$id]);
208
        $this->definitions[$id] = $definition;
209
    }
210 98
211
    /**
212 98
     * Sets multiple definitions at once.
213 98
     *
214 1
     * @param array $config definitions indexed by their ids
215
     *
216 98
     * @throws InvalidConfigException
217
     */
218 98
    protected function setMultiple(array $config): void
219
    {
220 98
        foreach ($config as $id => $definition) {
221
            if ($this->validate && !is_string($id)) {
222 98
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
223 98
            }
224 98
            $this->set($id, $definition);
225 98
        }
226
    }
227 98
228
    private function setDefaultDefinitions(): void
229
    {
230
        $container = $this->rootContainer ?? $this;
231
        $this->setMultiple([
232
            ContainerInterface::class => $container,
233
            Injector::class => new Injector($container),
234 98
        ]);
235
    }
236 98
237 36
    /**
238 36
     * @param mixed $definition
239 36
     *
240 36
     * @throws InvalidConfigException
241
     */
242
    private function validateDefinition($definition, ?string $id = null): void
243
    {
244 98
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
245 98
            [$class, $constructorArguments, $methodsAndProperties] = $definition;
246
            $definition = array_merge(
247
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
248
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
249
                $methodsAndProperties,
250 98
            );
251
        }
252 98
        DefinitionValidator::validate($definition, $id);
253 16
    }
254 3
255 3
    /**
256 3
     * @throws InvalidConfigException
257
     */
258
    private function validateMeta(array $meta): void
259
    {
260
        foreach ($meta as $key => $_value) {
261
            if (!in_array($key, self::ALLOWED_META, true)) {
262
                throw new InvalidConfigException(
263
                    sprintf(
264 98
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
265
                        $key,
266 8
                        $key,
267
                        $key,
268 8
                    )
269 8
                );
270
            }
271
        }
272
    }
273 8
274
    private function validateTags(array $tags): void
275 8
    {
276
        foreach ($tags as $tag) {
277 8
            if (!is_string($tag)) {
278 8
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
279 8
            }
280
        }
281
    }
282 8
283
    private function setTags(string $id, array $tags): void
284 4
    {
285
        foreach ($tags as $tag) {
286 4
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
287 4
                $this->tags[$tag][] = $id;
288
            }
289
        }
290
    }
291
292
    private function setResetter(string $id, Closure $resetter): void
293
    {
294
        $this->resetters[$id] = $resetter;
295
    }
296
297
    /**
298
     * Creates new instance by either interface name or alias.
299
     *
300
     * @param string $id The interface or an alias name that was previously registered.
301
     *
302 91
     * @throws CircularReferenceException
303
     * @throws InvalidConfigException
304 91
     * @throws NotFoundException
305 9
     *
306
     * @return mixed|object New built instance of the specified class.
307
     *
308 91
     * @internal
309 9
     */
310 2
    private function build(string $id)
311
    {
312 7
        if ($this->isTagAlias($id)) {
313 7
            return $this->getTaggedServices($id);
314
        }
315 7
316
        if (isset($this->building[$id])) {
317
            if ($id === ContainerInterface::class) {
318
                return $this;
319 91
            }
320
            throw new CircularReferenceException(sprintf(
321 91
                'Circular reference to "%s" detected while building: %s.',
322 91
                $id,
323 91
                implode(',', array_keys($this->building))
324
            ));
325
        }
326 91
327
        $this->building[$id] = 1;
328
        try {
329 91
            $object = $this->buildInternal($id);
330
        } finally {
331 91
            unset($this->building[$id]);
332
        }
333
334 9
        return $object;
335
    }
336 9
337 9
    private function isTagAlias(string $id): bool
338 9
    {
339 8
        return strpos($id, 'tag@') === 0;
340 8
    }
341
342
    private function getTaggedServices(string $tagAlias): array
343
    {
344 9
        $tag = substr($tagAlias, 4);
345
        $services = [];
346
        if (isset($this->tags[$tag])) {
347
            foreach ($this->tags[$tag] as $service) {
348
                $services[] = $this->get($service);
349
            }
350 91
        }
351
352 91
        return $services;
353 1
    }
354
355 91
    /**
356
     * @param mixed $definition
357
     */
358
    private function processDefinition($definition): void
359
    {
360
        if ($definition instanceof DeferredServiceProviderInterface) {
361
            $definition->register($this);
362
        }
363
    }
364
365 91
    /**
366
     * @param string $id
367 91
     *
368 50
     * @throws InvalidConfigException
369
     * @throws NotFoundException
370 91
     *
371 91
     * @return mixed|object
372
     */
373 91
    private function buildInternal(string $id)
374
    {
375
        if (!isset($this->definitions[$id])) {
376
            return $this->buildPrimitive($id);
377
        }
378
        $this->processDefinition($this->definitions[$id]);
379
380
        return $this->definitions[$id]->resolve($this->dependencyResolver);
381
    }
382
383
    /**
384 50
     * @param string $class
385
     *
386 50
     * @throws InvalidConfigException
387 48
     * @throws NotFoundException
388
     *
389 48
     * @return mixed|object
390
     */
391
    private function buildPrimitive(string $class)
392 4
    {
393
        if (class_exists($class)) {
394
            $definition = ArrayDefinition::fromPreparedData($class);
395 92
396
            return $definition->resolve($this->dependencyResolver);
397 92
        }
398 6
399
        throw new NotFoundException($class);
400 90
    }
401
402
    private function addProviders(array $providers, DependencyResolverInterface $dependencyResolver): void
403
    {
404
        foreach ($providers as $provider) {
405
            $this->addProvider($provider, $dependencyResolver);
406
        }
407
    }
408
409
    /**
410
     * Adds service provider to the container. Unless service provider is deferred
411
     * it would be immediately registered.
412
     *
413
     * @param mixed $providerDefinition
414 6
     *
415
     * @throws InvalidConfigException
416 6
     * @throws NotInstantiableException
417
     *
418 5
     * @see ServiceProviderInterface
419 1
     * @see DeferredServiceProviderInterface
420 1
     */
421
    private function addProvider($providerDefinition, DependencyResolverInterface $dependencyResolver): void
422
    {
423 4
        $provider = $this->buildProvider($providerDefinition, $dependencyResolver);
424
425 4
        if ($provider instanceof DeferredServiceProviderInterface) {
426
            foreach ($provider->provides() as $id) {
427
                $this->definitions[$id] = $provider;
428
            }
429
        } else {
430
            $provider->register($this);
431
        }
432
    }
433
434
    /**
435
     * Builds service provider by definition.
436 6
     *
437
     * @param mixed $providerDefinition class name or definition of provider.
438 6
     *
439 6
     * @throws InvalidConfigException
440
     *
441 5
     * @return ServiceProviderInterface instance of service provider;
442 5
     */
443 5
    private function buildProvider($providerDefinition, DependencyResolverInterface $dependencyResolver): ServiceProviderInterface
444 5
    {
445 5
        if ($this->validate) {
446 5
            $this->validateDefinition($providerDefinition);
447
        }
448
        $provider = DefinitionNormalizer::normalize($providerDefinition)->resolve($dependencyResolver);
449
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
450 5
            sprintf(
451
                'Service provider should be an instance of %s. %s given.',
452
                ServiceProviderInterface::class,
453
                $this->getVariableType($provider)
454
            )
455
        ));
456 6
457
        return $provider;
458 6
    }
459 5
460
    /**
461
     * @param mixed $variable
462 1
     */
463
    private function getVariableType($variable): string
464
    {
465
        if (is_object($variable)) {
466
            return get_class($variable);
467
        }
468
469
        return gettype($variable);
470
    }
471
472
    private function decorateLazy(string $id, DefinitionInterface $definition): DefinitionInterface
473
    {
474
        $factory = $this->getLazyLoadingValueHolderFactory();
475
        if (class_exists($id) || interface_exists($id)) {
476
            $class = $id;
477
        } elseif ($definition instanceof ArrayDefinition) {
478
            $class = $definition->getClass();
479
        } else {
480
            throw new \RuntimeException('Could not determinate object class');
481
        }
482
483
        return new LazyDefinitionDecorator($factory, $definition, $class);
484
    }
485
486
    private function getLazyLoadingValueHolderFactory(): LazyLoadingValueHolderFactory
487
    {
488
        if (!class_exists(LazyLoadingValueHolderFactory::class)) {
489
            throw new \RuntimeException('You should install `ocramius/proxy-manager` if you want to use lazy services.');
490
        }
491
        return $this->lazyFactory ??= new LazyLoadingValueHolderFactory();
492
    }
493
}
494