Test Failed
Push — lazy-services ( 4bdd4b...592c23 )
by Dmitriy
02:36
created

Container   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Importance

Changes 14
Bugs 2 Features 0
Metric Value
eloc 147
c 14
b 2
f 0
dl 0
loc 448
rs 2.64
wmc 72

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getVariableType() 0 7 2
A get() 0 17 6
A getLazyLoadingValueHolderFactory() 0 6 2
A has() 0 8 3
A addProviderDefinitions() 0 4 1
A setTags() 0 5 4
A addProviders() 0 20 6
B set() 0 24 7
A validateTags() 0 5 3
A setDefaultDefinitions() 0 3 1
B buildProvider() 0 27 7
A setResetter() 0 3 1
A decorateLazy() 0 12 4
A build() 0 25 4
A validateMeta() 0 10 3
A buildInternal() 0 9 2
A isTagAlias() 0 3 1
A setMultiple() 0 7 4
A validateDefinition() 0 16 5
A getTaggedServices() 0 11 3
A buildPrimitive() 0 11 2
A __construct() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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