Test Failed
Pull Request — master (#232)
by Dmitriy
02:22
created

Container::decorateLazy()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
nc 3
nop 2
dl 0
loc 12
ccs 0
cts 0
cp 0
crap 20
rs 10
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\ServiceProviderInterface;
11
use Yiisoft\Factory\Definition\ArrayDefinition;
12
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...
13
use Yiisoft\Factory\Definition\DefinitionInterface;
14
use Yiisoft\Factory\Definition\DefinitionValidator;
15
use Yiisoft\Factory\Exception\CircularReferenceException;
16
use Yiisoft\Factory\Exception\InvalidConfigException;
17
use Yiisoft\Factory\Exception\NotFoundException;
18
use Yiisoft\Factory\Exception\NotInstantiableException;
19
20
use function array_key_exists;
21
use function array_keys;
22
use function class_exists;
23
use function get_class;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function is_object;
28
use function is_string;
29
30
/**
31
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
32
 */
33
final class Container implements ContainerInterface
34
{
35
    private const META_TAGS = 'tags';
36
    private const META_RESET = 'reset';
37
    private const META_LAZY = 'lazy';
38
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET, self::META_LAZY];
39
40
    /**
41
     * @var array object definitions indexed by their types
42
     */
43
    private array $definitions = [];
44
    /**
45
     * @var array used to collect ids instantiated during build
46
     * to detect circular references
47
     */
48
    private array $building = [];
49
50
    /**
51
     * @var bool $validate Validate definitions when set
52
     */
53
    private bool $validate;
54
55
    /**
56
     * @var object[]
57
     */
58
    private array $instances = [];
59
60
    private array $tags;
61
62
    private array $resetters = [];
63
    /** @psalm-suppress PropertyNotSetInConstructor */
64
    private DependencyResolver $dependencyResolver;
65
    private LazyLoadingValueHolderFactory $lazyFactory;
66
67
    /**
68
     * Container constructor.
69
     *
70
     * @param array $definitions Definitions to put into container.
71
     * @param array $providers Service providers to get definitions from.
72
     * lookup to when resolving dependencies. If provided the current container
73
     * is no longer queried for dependencies.
74 95
     *
75
     * @throws InvalidConfigException
76
     *
77
     * @psalm-suppress PropertyNotSetInConstructor
78
     */
79
    public function __construct(
80 95
        array $definitions = [],
81 95
        array $providers = [],
82 95
        array $tags = [],
83 95
        bool $validate = true
84 89
    ) {
85 88
        $this->tags = $tags;
86
        $this->validate = $validate;
87
        $this->setDefaultDefinitions();
88
        $this->setMultiple($definitions);
89
        $this->addProviders($providers);
90
    }
91
92
    /**
93
     * Returns a value indicating whether the container has the definition of the specified name.
94
     *
95
     * @param string $id class name, interface name or alias name
96 28
     *
97
     * @return bool whether the container is able to provide instance of class specified.
98 28
     *
99 2
     * @see set()
100 2
     */
101
    public function has($id): bool
102
    {
103 26
        if ($this->isTagAlias($id)) {
104
            $tag = substr($id, 4);
105
            return isset($this->tags[$tag]);
106
        }
107
108
        return isset($this->definitions[$id]) || class_exists($id);
109
    }
110
111
    /**
112
     * Returns an instance by either interface name or alias.
113
     *
114
     * Same instance of the class will be returned each time this method is called.
115
     *
116
     * @param string $id The interface or an alias name that was previously registered.
117
     *
118
     * @throws CircularReferenceException
119
     * @throws InvalidConfigException
120
     * @throws NotFoundException
121
     * @throws NotInstantiableException
122
     *
123
     * @return mixed|object An instance of the requested interface.
124 84
     *
125
     * @psalm-template T
126 84
     * @psalm-param string|class-string<T> $id
127 4
     * @psalm-return ($id is class-string ? T : mixed)
128 4
     */
129 4
    public function get($id)
130 4
    {
131
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
132
            $resetters = [];
133 4
            foreach ($this->resetters as $serviceId => $callback) {
134
                if (isset($this->instances[$serviceId])) {
135
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
136 84
                }
137 84
            }
138
            return new StateResetter($resetters, $this);
139
        }
140 82
141
        if (!array_key_exists($id, $this->instances)) {
142
            $this->instances[$id] = $this->build($id);
143
        }
144
145
        return $this->instances[$id];
146
    }
147
148
    /**
149
     * Sets a definition to the container. Definition may be defined multiple ways.
150
     *
151
     * @param string $id
152
     * @param mixed $definition
153 95
     *
154
     * @throws InvalidConfigException
155 95
     *
156 95
     * @see `DefinitionNormalizer::normalize()`
157 95
     */
158 95
    protected function set(string $id, $definition): void
159
    {
160
        [$definition, $meta] = DefinitionParser::parse($definition);
161 95
        if ($this->validate) {
162 8
            $this->validateDefinition($definition, $id);
163 8
            $this->validateMeta($meta);
164
        }
165 8
        $definition = DefinitionNormalizer::normalize($definition, $id);
166
167 95
        if (isset($meta[self::META_TAGS])) {
168 5
            if ($this->validate) {
169
                $this->validateTags($meta[self::META_TAGS]);
170
            }
171 95
            $this->setTags($id, $meta[self::META_TAGS]);
172 95
        }
173 95
        if (isset($meta[self::META_RESET])) {
174
            $this->setResetter($id, $meta[self::META_RESET]);
175
        }
176
        if (isset($meta[self::META_LAZY]) && $meta[self::META_LAZY] === true) {
177
            $definition = $this->decorateLazy($id, $definition);
178
        }
179
180
        unset($this->instances[$id]);
181
        $this->definitions[$id] = $definition;
182 95
    }
183
184 95
    /**
185 88
     * Sets multiple definitions at once.
186 1
     *
187
     * @param array $config definitions indexed by their ids
188 87
     *
189
     * @throws InvalidConfigException
190 89
     */
191
    protected function setMultiple(array $config): void
192 95
    {
193
        foreach ($config as $id => $definition) {
194 95
            if ($this->validate && !is_string($id)) {
195 95
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
196
            }
197
            $this->set($id, $definition);
198
        }
199
    }
200
201
    private function setDefaultDefinitions(): void
202 95
    {
203
        $this->set(ContainerInterface::class, $this);
204 95
    }
205 39
206 39
    /**
207 39
     * @param mixed $definition
208 39
     *
209
     * @throws InvalidConfigException
210
     */
211
    private function validateDefinition($definition, ?string $id = null): void
212
    {
213 95
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
214
            [$class, $constructorArguments, $methodsAndProperties] = $definition;
215
            $definition = array_merge(
216
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
217 95
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
218 95
                $methodsAndProperties,
219
            );
220
        }
221
222
        if ($definition instanceof ExtensibleService) {
223 95
            throw new InvalidConfigException('Invalid definition. ExtensibleService is only allowed in provider extensions.');
224
        }
225 95
226 16
        DefinitionValidator::validate($definition, $id);
227 3
    }
228 3
229 3
    /**
230
     * @throws InvalidConfigException
231
     */
232
    private function validateMeta(array $meta): void
233
    {
234
        foreach ($meta as $key => $_value) {
235
            if (!in_array($key, self::ALLOWED_META, true)) {
236
                throw new InvalidConfigException(
237 95
                    sprintf(
238
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
239 8
                        $key,
240
                        $key,
241 8
                        $key,
242 8
                    )
243
                );
244
            }
245
        }
246 8
    }
247
248 8
    private function validateTags(array $tags): void
249
    {
250 8
        foreach ($tags as $tag) {
251 8
            if (!is_string($tag)) {
252 8
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
253
            }
254
        }
255 8
    }
256
257 4
    private function setTags(string $id, array $tags): void
258
    {
259 4
        foreach ($tags as $tag) {
260 4
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
261
                $this->tags[$tag][] = $id;
262
            }
263
        }
264
    }
265
266
    private function setResetter(string $id, Closure $resetter): void
267
    {
268
        $this->resetters[$id] = $resetter;
269
    }
270
271
    /**
272
     * Creates new instance by either interface name or alias.
273
     *
274
     * @param string $id The interface or an alias name that was previously registered.
275 84
     *
276
     * @throws CircularReferenceException
277 84
     * @throws InvalidConfigException
278 9
     * @throws NotFoundException
279
     *
280
     * @return mixed|object New built instance of the specified class.
281 83
     *
282 81
     * @internal
283 81
     */
284
    private function build(string $id)
285 7
    {
286 7
        if ($this->isTagAlias($id)) {
287
            return $this->getTaggedServices($id);
288 7
        }
289
290
        if (isset($this->building[$id])) {
291
            if ($id === ContainerInterface::class) {
292 83
                return $this;
293
            }
294 83
            throw new CircularReferenceException(sprintf(
295 81
                'Circular reference to "%s" detected while building: %s.',
296 83
                $id,
297
                implode(',', array_keys($this->building))
298
            ));
299 81
        }
300
301
        $this->building[$id] = 1;
302 88
        try {
303
            $object = $this->buildInternal($id);
304 88
        } finally {
305
            unset($this->building[$id]);
306
        }
307 9
308
        return $object;
309 9
    }
310 9
311 9
    private function isTagAlias(string $id): bool
312 8
    {
313 8
        return strpos($id, 'tag@') === 0;
314
    }
315
316
    private function getTaggedServices(string $tagAlias): array
317 9
    {
318
        $tag = substr($tagAlias, 4);
319
        $services = [];
320
        if (isset($this->tags[$tag])) {
321
            foreach ($this->tags[$tag] as $service) {
322
                $services[] = $this->get($service);
323
            }
324
        }
325
326
        return $services;
327
    }
328 83
329
    /**
330 83
     * @param string $id
331 50
     *
332
     * @throws InvalidConfigException
333 81
     * @throws NotFoundException
334
     *
335 81
     * @return mixed|object
336
     */
337 81
    private function buildInternal(string $id)
338
    {
339
        if (!isset($this->definitions[$id])) {
340
            return $this->buildPrimitive($id);
341
        }
342
        $definition = DefinitionNormalizer::normalize($this->definitions[$id], $id);
0 ignored issues
show
Unused Code introduced by
The assignment to $definition is dead and can be removed.
Loading history...
343
        /** @psalm-suppress RedundantPropertyInitializationCheck */
344
        $this->dependencyResolver ??= new DependencyResolver($this->get(ContainerInterface::class));
345
346
        return $this->definitions[$id]->resolve($this->dependencyResolver);
347
    }
348 50
349
    /**
350 50
     * @param string $class
351 48
     *
352
     * @throws InvalidConfigException
353 48
     * @throws NotFoundException
354
     *
355 48
     * @return mixed|object
356
     */
357
    private function buildPrimitive(string $class)
358 5
    {
359
        if (class_exists($class)) {
360
            $definition = ArrayDefinition::fromPreparedData($class);
361 89
            /** @psalm-suppress RedundantPropertyInitializationCheck */
362
            $this->dependencyResolver ??= new DependencyResolver($this->get(ContainerInterface::class));
363 89
364 89
            return $definition->resolve($this->dependencyResolver);
365 5
        }
366 5
367 5
        throw new NotFoundException($class);
368
    }
369
370 89
    private function addProviders(array $providers): void
371 5
    {
372 4
        $extensions = [];
373 1
        foreach ($providers as $provider) {
374
            $providerInstance = $this->buildProvider($provider);
375
            $extensions[] = $providerInstance->getExtensions();
376 3
            $this->addProviderDefinitions($providerInstance);
377 3
        }
378
379
        foreach ($extensions as $providerExtensions) {
380 3
            foreach ($providerExtensions as $id => $extension) {
381
                if (!isset($this->definitions[$id])) {
382
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
383 88
                }
384
385
                if (!$this->definitions[$id] instanceof ExtensibleService) {
386
                    $this->definitions[$id] = new ExtensibleService($this->definitions[$id]);
387
                }
388
389
                $this->definitions[$id]->addExtension($extension);
390
            }
391
        }
392
    }
393
394
    /**
395 5
     * Adds service provider definitions to the container.
396
     *
397 5
     * @param object $provider
398 5
     *
399 5
     * @throws InvalidConfigException
400
     * @throws NotInstantiableException
401
     *
402
     * @see ServiceProviderInterface
403
     */
404
    private function addProviderDefinitions($provider): void
405
    {
406
        $definitions = $provider->getDefinitions();
407
        $this->setMultiple($definitions);
408
    }
409
410
    /**
411
     * Builds service provider by definition.
412 5
     *
413
     * @param mixed $provider Class name or instance of provider.
414 5
     *
415
     * @throws InvalidConfigException If provider argument is not valid.
416
     *
417
     * @return ServiceProviderInterface Instance of service provider.
418
     *
419
     * @psalm-suppress MoreSpecificReturnType
420
     */
421
    private function buildProvider($provider): ServiceProviderInterface
422
    {
423
        if ($this->validate && !(is_string($provider) || is_object($provider) && $provider instanceof ServiceProviderInterface)) {
424 5
            throw new InvalidConfigException(
425 5
                sprintf(
426
                    'Service provider should be a class name or an instance of %s. %s given.',
427
                    ServiceProviderInterface::class,
428
                    $this->getVariableType($provider)
429
                )
430
            );
431
        }
432
433
        $providerInstance = is_object($provider) ? $provider : new $provider();
434
        if (!$providerInstance instanceof ServiceProviderInterface) {
435
            throw new InvalidConfigException(
436
                sprintf(
437
                    'Service provider should be an instance of %s. %s given.',
438 5
                    ServiceProviderInterface::class,
439
                    $this->getVariableType($providerInstance)
440
                )
441
            );
442
        }
443
444 1
        /**
445
         * @psalm-suppress LessSpecificReturnStatement
446 1
         */
447
        return $providerInstance;
448
    }
449
450 1
    /**
451
     * @param mixed $variable
452
     */
453
    private function getVariableType($variable): string
454
    {
455
        if (is_object($variable)) {
456
            return get_class($variable);
457
        }
458
459
        return gettype($variable);
460
    }
461
462
    private function decorateLazy(string $id, DefinitionInterface $definition): DefinitionInterface
463
    {
464
        $factory = $this->getLazyLoadingValueHolderFactory();
465
        if (class_exists($id) || interface_exists($id)) {
466
            $class = $id;
467
        } elseif ($definition instanceof ArrayDefinition) {
468
            $class = $definition->getClass();
469
        } else {
470
            throw new \RuntimeException('Could not determinate object class');
471
        }
472
473
        return new LazyDefinitionDecorator($factory, $definition, $class);
474
    }
475
476
    private function getLazyLoadingValueHolderFactory(): LazyLoadingValueHolderFactory
477
    {
478
        if (!class_exists(LazyLoadingValueHolderFactory::class)) {
479
            throw new \RuntimeException('You should install `ocramius/proxy-manager` if you want to use lazy services.');
480
        }
481
        return $this->lazyFactory ??= new LazyLoadingValueHolderFactory();
482
    }
483
}
484