Passed
Push — refactoring ( eae241...68edd5 )
by Sergei
02:21
created

Container   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Test Coverage

Coverage 99.22%

Importance

Changes 11
Bugs 2 Features 0
Metric Value
eloc 111
c 11
b 2
f 0
dl 0
loc 378
ccs 128
cts 129
cp 0.9922
rs 8.4
wmc 50

20 Methods

Rating   Name   Duplication   Size   Complexity  
A has() 0 8 3
A get() 0 17 6
A setTags() 0 5 4
A set() 0 15 3
A addProviders() 0 4 2
A validateTags() 0 5 3
A setDefaultDefinitions() 0 6 1
A buildProvider() 0 12 1
A setResetter() 0 3 1
A delegateLookup() 0 11 3
A build() 0 25 4
A processDefinition() 0 4 2
A isTagAlias() 0 3 1
A buildInternal() 0 9 2
A setMultiple() 0 7 3
A getTaggedServices() 0 11 3
A getVariableType() 0 7 2
A buildPrimitive() 0 9 2
A __construct() 0 15 1
A addProvider() 0 10 3

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 Psr\Container\ContainerInterface;
9
use Yiisoft\Di\Contracts\DeferredServiceProviderInterface;
10
use Yiisoft\Di\Contracts\ServiceProviderInterface;
11
use Yiisoft\Factory\Definition\ArrayDefinition;
12
use Yiisoft\Factory\Definition\Normalizer;
13
use Yiisoft\Factory\Exception\CircularReferenceException;
14
use Yiisoft\Factory\Exception\InvalidConfigException;
15
use Yiisoft\Factory\Exception\NotFoundException;
16
use Yiisoft\Factory\Exception\NotInstantiableException;
17
use Yiisoft\Injector\Injector;
18
19
use function array_key_exists;
20
use function array_keys;
21
use function assert;
22
use function class_exists;
23
use function get_class;
24
use function implode;
25
use function in_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 extends AbstractContainerConfigurator implements ContainerInterface
33
{
34
    private const META_TAGS = 'tags';
35
    private const META_RESET = 'reset';
36
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET];
37
38
    private DefinitionParser $definitionParser;
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
     * @var object[]
51
     */
52
    private array $instances = [];
53
54
    private array $tags;
55
56
    private array $resetters = [];
57
58
    private ?CompositeContainer $rootContainer = null;
59
60
    /**
61
     * Container constructor.
62
     *
63
     * @param array $definitions Definitions to put into container.
64
     * @param ServiceProviderInterface[]|string[] $providers Service providers
65
     * to get definitions from.
66
     * @param ContainerInterface|null $rootContainer Root container to delegate
67
     * lookup to when resolving dependencies. If provided the current container
68
     * is no longer queried for dependencies.
69
     *
70
     * @throws InvalidConfigException
71
     */
72 93
    public function __construct(
73
        array $definitions = [],
74
        array $providers = [],
75
        array $tags = [],
76
        ContainerInterface $rootContainer = null
77
    ) {
78 93
        $this->definitionParser = new DefinitionParser(self::ALLOWED_META);
79 93
        $this->tags = $tags;
80 93
        $this->delegateLookup($rootContainer);
81 93
        $this->setDefaultDefinitions();
82 93
        $this->setMultiple($definitions);
83 90
        $this->addProviders($providers);
84
85
        // Prevent circular reference to ContainerInterface
86 88
        $this->get(ContainerInterface::class);
87 88
    }
88
89
    /**
90
     * Returns a value indicating whether the container has the definition of the specified name.
91
     *
92
     * @param string $id class name, interface name or alias name
93
     *
94
     * @return bool whether the container is able to provide instance of class specified.
95
     *
96
     * @see set()
97
     */
98 32
    public function has($id): bool
99
    {
100 32
        if ($this->isTagAlias($id)) {
101 2
            $tag = substr($id, 4);
102 2
            return isset($this->tags[$tag]);
103
        }
104
105 30
        return isset($this->definitions[$id]) || class_exists($id);
106
    }
107
108
    /**
109
     * Returns an instance by either interface name or alias.
110
     *
111
     * Same instance of the class will be returned each time this method is called.
112
     *
113
     * @param string $id The interface or an alias name that was previously registered.
114
     *
115
     * @throws CircularReferenceException
116
     * @throws InvalidConfigException
117
     * @throws NotFoundException
118
     * @throws NotInstantiableException
119
     *
120
     * @return mixed|object An instance of the requested interface.
121
     *
122
     * @psalm-template T
123
     * @psalm-param string|class-string<T> $id
124
     * @psalm-return ($id is class-string ? T : mixed)
125
     */
126 89
    public function get($id)
127
    {
128 89
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
129 4
            $resetters = [];
130 4
            foreach ($this->resetters as $serviceId => $callback) {
131 4
                if (isset($this->instances[$serviceId])) {
132 4
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
133
                }
134
            }
135 4
            return new StateResetter($resetters, $this);
136
        }
137
138 89
        if (!array_key_exists($id, $this->instances)) {
139 89
            $this->instances[$id] = $this->build($id);
140
        }
141
142 89
        return $this->instances[$id];
143
    }
144
145
    /**
146
     * Delegate service lookup to another container.
147
     *
148
     * @param ContainerInterface $container
149
     */
150 93
    protected function delegateLookup(?ContainerInterface $container): void
151
    {
152 93
        if ($container === null) {
153 93
            return;
154
        }
155 8
        if ($this->rootContainer === null) {
156 8
            $this->rootContainer = new CompositeContainer();
157 8
            $this->setDefaultDefinitions();
158
        }
159
160 8
        $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

160
        $this->rootContainer->/** @scrutinizer ignore-call */ 
161
                              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...
161 8
    }
162
163
    /**
164
     * Sets a definition to the container. Definition may be defined multiple ways.
165
     *
166
     * @param string $id
167
     * @param mixed $definition
168
     *
169
     * @throws InvalidConfigException
170
     *
171
     * @see `Normalizer::normalize()`
172
     */
173 93
    protected function set(string $id, $definition): void
174
    {
175 93
        [$definition, $meta] = $this->definitionParser->parse($definition);
176
177 93
        Normalizer::validate($definition);
178 93
        if (isset($meta[self::META_TAGS])) {
179 8
            $this->validateTags($meta[self::META_TAGS]);
180 8
            $this->setTags($id, $meta[self::META_TAGS]);
181
        }
182 93
        if (isset($meta[self::META_RESET])) {
183 5
            $this->setResetter($id, $meta[self::META_RESET]);
184
        }
185
186 93
        unset($this->instances[$id]);
187 93
        $this->definitions[$id] = $definition;
188 93
    }
189
190
    /**
191
     * Sets multiple definitions at once.
192
     *
193
     * @param array $config definitions indexed by their ids
194
     *
195
     * @throws InvalidConfigException
196
     */
197 93
    protected function setMultiple(array $config): void
198
    {
199 93
        foreach ($config as $id => $definition) {
200 93
            if (!is_string($id)) {
201 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
202
            }
203 93
            $this->set($id, $definition);
204
        }
205 93
    }
206
207 93
    private function setDefaultDefinitions(): void
208
    {
209 93
        $container = $this->rootContainer ?? $this;
210 93
        $this->setMultiple([
211 93
            ContainerInterface::class => $container,
212 93
            Injector::class => new Injector($container),
213
        ]);
214 93
    }
215
216 8
    private function validateTags(array $tags): void
217
    {
218 8
        foreach ($tags as $tag) {
219 8
            if (!is_string($tag)) {
220
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
221
            }
222
        }
223 8
    }
224
225 8
    private function setTags(string $id, array $tags): void
226
    {
227 8
        foreach ($tags as $tag) {
228 8
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
229 8
                $this->tags[$tag][] = $id;
230
            }
231
        }
232 8
    }
233
234 4
    private function setResetter(string $id, Closure $resetter): void
235
    {
236 4
        $this->resetters[$id] = $resetter;
237 4
    }
238
239
    /**
240
     * Creates new instance by either interface name or alias.
241
     *
242
     * @param string $id The interface or an alias name that was previously registered.
243
     *
244
     * @throws CircularReferenceException
245
     * @throws InvalidConfigException
246
     * @throws NotFoundException
247
     *
248
     * @return mixed|object New built instance of the specified class.
249
     *
250
     * @internal
251
     */
252 89
    private function build(string $id)
253
    {
254 89
        if ($this->isTagAlias($id)) {
255 9
            return $this->getTaggedServices($id);
256
        }
257
258 89
        if (isset($this->building[$id])) {
259 9
            if ($id === ContainerInterface::class) {
260 2
                return $this;
261
            }
262 7
            throw new CircularReferenceException(sprintf(
263 7
                'Circular reference to "%s" detected while building: %s.',
264
                $id,
265 7
                implode(',', array_keys($this->building))
266
            ));
267
        }
268
269 89
        $this->building[$id] = 1;
270
        try {
271 89
            $object = $this->buildInternal($id);
272 89
        } finally {
273 89
            unset($this->building[$id]);
274
        }
275
276 89
        return $object;
277
    }
278
279 89
    private function isTagAlias(string $id): bool
280
    {
281 89
        return strpos($id, 'tag@') === 0;
282
    }
283
284 9
    private function getTaggedServices(string $tagAlias): array
285
    {
286 9
        $tag = substr($tagAlias, 4);
287 9
        $services = [];
288 9
        if (isset($this->tags[$tag])) {
289 8
            foreach ($this->tags[$tag] as $service) {
290 8
                $services[] = $this->get($service);
291
            }
292
        }
293
294 9
        return $services;
295
    }
296
297
    /**
298
     * @param mixed $definition
299
     */
300 89
    private function processDefinition($definition): void
301
    {
302 89
        if ($definition instanceof DeferredServiceProviderInterface) {
303 1
            $definition->register($this);
304
        }
305 89
    }
306
307
    /**
308
     * @param string $id
309
     *
310
     * @throws InvalidConfigException
311
     * @throws NotFoundException
312
     *
313
     * @return mixed|object
314
     */
315 89
    private function buildInternal(string $id)
316
    {
317 89
        if (!isset($this->definitions[$id])) {
318 51
            return $this->buildPrimitive($id);
319
        }
320 89
        $this->processDefinition($this->definitions[$id]);
321 89
        $definition = Normalizer::normalize($this->definitions[$id], $id, []);
322
323 89
        return $definition->resolve($this->rootContainer ?? $this);
324
    }
325
326
    /**
327
     * @param string $class
328
     *
329
     * @throws InvalidConfigException
330
     * @throws NotFoundException
331
     *
332
     * @return mixed|object
333
     */
334 51
    private function buildPrimitive(string $class)
335
    {
336 51
        if (class_exists($class)) {
337 49
            $definition = new ArrayDefinition([ArrayDefinition::CLASS_NAME => $class]);
338
339 49
            return $definition->resolve($this->rootContainer ?? $this);
340
        }
341
342 4
        throw new NotFoundException($class);
343
    }
344
345 90
    private function addProviders(array $providers): void
346
    {
347 90
        foreach ($providers as $provider) {
348 6
            $this->addProvider($provider);
349
        }
350 88
    }
351
352
    /**
353
     * Adds service provider to the container. Unless service provider is deferred
354
     * it would be immediately registered.
355
     *
356
     * @param mixed $providerDefinition
357
     *
358
     * @throws InvalidConfigException
359
     * @throws NotInstantiableException
360
     *
361
     * @see ServiceProviderInterface
362
     * @see DeferredServiceProviderInterface
363
     */
364 6
    private function addProvider($providerDefinition): void
365
    {
366 6
        $provider = $this->buildProvider($providerDefinition);
367
368 5
        if ($provider instanceof DeferredServiceProviderInterface) {
369 1
            foreach ($provider->provides() as $id) {
370 1
                $this->definitions[$id] = $provider;
371
            }
372
        } else {
373 4
            $provider->register($this);
374
        }
375 4
    }
376
377
    /**
378
     * Builds service provider by definition.
379
     *
380
     * @param mixed $providerDefinition class name or definition of provider.
381
     *
382
     * @throws InvalidConfigException
383
     *
384
     * @return ServiceProviderInterface instance of service provider;
385
     */
386 6
    private function buildProvider($providerDefinition): ServiceProviderInterface
387
    {
388 6
        $provider = Normalizer::normalize($providerDefinition)->resolve($this);
389 5
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
390 5
            sprintf(
391 5
                'Service provider should be an instance of %s. %s given.',
392 5
                ServiceProviderInterface::class,
393 5
                $this->getVariableType($provider)
394
            )
395
        ));
396
397 5
        return $provider;
398
    }
399
400
    /**
401
     * @param mixed $variable
402
     */
403 6
    private function getVariableType($variable): string
404
    {
405 6
        if (is_object($variable)) {
406 5
            return get_class($variable);
407
        }
408
409 1
        return gettype($variable);
410
    }
411
}
412