Test Failed
Push — lazy-services ( 15f8a6 )
by Dmitriy
03:06
created

Container::build()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 15
c 2
b 0
f 0
nc 5
nop 1
dl 0
loc 25
rs 9.7666
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
     * is no longer queried for dependencies.
79
     *
80
     * @throws InvalidConfigException
81
     */
82
    public function __construct(
83
        array $definitions = [],
84
        array $providers = [],
85
        array $tags = [],
86
        ContainerInterface $rootContainer = null,
87
        bool $validate = true
88
    ) {
89
        $this->tags = $tags;
90
        $this->validate = $validate;
91
        $this->delegateLookup($rootContainer);
92
        $this->setDefaultDefinitions();
93
        $this->setMultiple($definitions);
94
        $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
     * @return bool whether the container is able to provide instance of class specified.
106
     *
107
     * @see set()
108
     */
109
    public function has($id): bool
110
    {
111
        if ($this->isTagAlias($id)) {
112
            $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
     * @psalm-template T
134
     * @psalm-param string|class-string<T> $id
135
     * @psalm-return ($id is class-string ? T : mixed)
136
     */
137
    public function get($id)
138
    {
139
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
140
            $resetters = [];
141
            foreach ($this->resetters as $serviceId => $callback) {
142
                if (isset($this->instances[$serviceId])) {
143
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
144
                }
145
            }
146
            return new StateResetter($resetters, $this);
147
        }
148
149
        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
     * Delegate service lookup to another container.
158
     *
159
     * @param ContainerInterface $container
160
     */
161
    protected function delegateLookup(?ContainerInterface $container): void
162
    {
163
        if ($container !== null) {
164
            if ($this->rootContainer === null) {
165
                $this->rootContainer = new CompositeContainer();
166
                $this->setDefaultDefinitions();
167
            }
168
169
            $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
     * @throws InvalidConfigException
182
     *
183
     * @see `DefinitionNormalizer::normalize()`
184
     */
185
    protected function set(string $id, $definition): void
186
    {
187
        [$definition, $meta] = DefinitionParser::parse($definition);
188
        if ($this->validate) {
189
            $this->validateDefinition($definition, $id);
190
            $this->validateMeta($meta);
191
        }
192
        $definition = DefinitionNormalizer::normalize($definition, $id);
193
194
        if (isset($meta[self::META_TAGS])) {
195
            if ($this->validate) {
196
                $this->validateTags($meta[self::META_TAGS]);
197
            }
198
            $this->setTags($id, $meta[self::META_TAGS]);
199
        }
200
        if (isset($meta[self::META_RESET])) {
201
            $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
211
    /**
212
     * Sets multiple definitions at once.
213
     *
214
     * @param array $config definitions indexed by their ids
215
     *
216
     * @throws InvalidConfigException
217
     */
218
    protected function setMultiple(array $config): void
219
    {
220
        foreach ($config as $id => $definition) {
221
            if ($this->validate && !is_string($id)) {
222
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
223
            }
224
            $this->set($id, $definition);
225
        }
226
    }
227
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
        ]);
235
    }
236
237
    /**
238
     * @param mixed $definition
239
     *
240
     * @throws InvalidConfigException
241
     */
242
    private function validateDefinition($definition, ?string $id = null): void
243
    {
244
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
245
            [$class, $constructorArguments, $methodsAndProperties] = $definition;
246
            $definition = array_merge(
247
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
248
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
249
                $methodsAndProperties,
250
            );
251
        }
252
        DefinitionValidator::validate($definition, $id);
253
    }
254
255
    /**
256
     * @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
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
265
                        $key,
266
                        $key,
267
                        $key,
268
                    )
269
                );
270
            }
271
        }
272
    }
273
274
    private function validateTags(array $tags): void
275
    {
276
        foreach ($tags as $tag) {
277
            if (!is_string($tag)) {
278
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
279
            }
280
        }
281
    }
282
283
    private function setTags(string $id, array $tags): void
284
    {
285
        foreach ($tags as $tag) {
286
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
287
                $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
     * @throws CircularReferenceException
303
     * @throws InvalidConfigException
304
     * @throws NotFoundException
305
     *
306
     * @return mixed|object New built instance of the specified class.
307
     *
308
     * @internal
309
     */
310
    private function build(string $id)
311
    {
312
        if ($this->isTagAlias($id)) {
313
            return $this->getTaggedServices($id);
314
        }
315
316
        if (isset($this->building[$id])) {
317
            if ($id === ContainerInterface::class) {
318
                return $this;
319
            }
320
            throw new CircularReferenceException(sprintf(
321
                'Circular reference to "%s" detected while building: %s.',
322
                $id,
323
                implode(',', array_keys($this->building))
324
            ));
325
        }
326
327
        $this->building[$id] = 1;
328
        try {
329
            $object = $this->buildInternal($id);
330
        } finally {
331
            unset($this->building[$id]);
332
        }
333
334
        return $object;
335
    }
336
337
    private function isTagAlias(string $id): bool
338
    {
339
        return strpos($id, 'tag@') === 0;
340
    }
341
342
    private function getTaggedServices(string $tagAlias): array
343
    {
344
        $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
        }
351
352
        return $services;
353
    }
354
355
    /**
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
    /**
366
     * @param string $id
367
     *
368
     * @throws InvalidConfigException
369
     * @throws NotFoundException
370
     *
371
     * @return mixed|object
372
     */
373
    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
     * @param string $class
385
     *
386
     * @throws InvalidConfigException
387
     * @throws NotFoundException
388
     *
389
     * @return mixed|object
390
     */
391
    private function buildPrimitive(string $class)
392
    {
393
        if (class_exists($class)) {
394
            $definition = ArrayDefinition::fromPreparedData($class);
395
396
            return $definition->resolve($this->dependencyResolver);
397
        }
398
399
        throw new NotFoundException($class);
400
    }
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
     *
415
     * @throws InvalidConfigException
416
     * @throws NotInstantiableException
417
     *
418
     * @see ServiceProviderInterface
419
     * @see DeferredServiceProviderInterface
420
     */
421
    private function addProvider($providerDefinition, DependencyResolverInterface $dependencyResolver): void
422
    {
423
        $provider = $this->buildProvider($providerDefinition, $dependencyResolver);
424
425
        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
     *
437
     * @param mixed $providerDefinition class name or definition of provider.
438
     *
439
     * @throws InvalidConfigException
440
     *
441
     * @return ServiceProviderInterface instance of service provider;
442
     */
443
    private function buildProvider($providerDefinition, DependencyResolverInterface $dependencyResolver): ServiceProviderInterface
444
    {
445
        if ($this->validate) {
446
            $this->validateDefinition($providerDefinition);
447
        }
448
        $provider = DefinitionNormalizer::normalize($providerDefinition)->resolve($dependencyResolver);
449
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
450
            sprintf(
451
                'Service provider should be an instance of %s. %s given.',
452
                ServiceProviderInterface::class,
453
                $this->getVariableType($provider)
454
            )
455
        ));
456
457
        return $provider;
458
    }
459
460
    /**
461
     * @param mixed $variable
462
     */
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