Passed
Pull Request — master (#120)
by Dmitriy
14:21
created

Container   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Test Coverage

Coverage 97.56%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 90
dl 0
loc 319
ccs 80
cts 82
cp 0.9756
rs 8.48
c 4
b 2
f 0
wmc 49

20 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 10 2
A getId() 0 3 2
A has() 0 3 2
A assignInstanceTag() 0 4 2
A addProviders() 0 4 2
A set() 0 9 2
A getDefinitionWithoutInstanceTag() 0 8 2
A invalidateInstance() 0 7 4
A hasInstanceTag() 0 11 4
A buildProvider() 0 10 2
A delegateLookup() 0 7 2
A build() 0 15 2
A processDefinition() 0 4 2
A buildInternal() 0 9 2
A setMultiple() 0 4 2
B validateDefinition() 0 23 7
A getInstanceTag() 0 3 1
A buildPrimitive() 0 9 2
A __construct() 0 9 2
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 Psr\Container\ContainerInterface;
8
use Yiisoft\Di\Contracts\DeferredServiceProviderInterface;
9
use Yiisoft\Di\Contracts\ServiceProviderInterface;
10
use Yiisoft\Factory\Definitions\DynamicReference;
11
use Yiisoft\Factory\Definitions\Reference;
12
use Yiisoft\Factory\Exceptions\CircularReferenceException;
13
use Yiisoft\Factory\Exceptions\InvalidConfigException;
14
use Yiisoft\Factory\Exceptions\NotFoundException;
15
use Yiisoft\Factory\Exceptions\NotInstantiableException;
16
use Yiisoft\Factory\Definitions\Normalizer;
17
use Yiisoft\Factory\Definitions\ArrayDefinition;
18
19
/**
20
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
21
 */
22
class Container extends AbstractContainerConfigurator implements ContainerInterface
23
{
24
    /**
25
     * @var array object definitions indexed by their types
26
     */
27
    private array $definitions = [];
28
29
    private array $assignedInstanceTags = [];
30
31
    private array $instanceTagCallbacks = [];
32
33
    /**
34
     * @var array used to collect ids instantiated during build
35
     * to detect circular references
36
     */
37
    private array $building = [];
38
39
    private ?ContainerInterface $rootContainer = null;
40
41
    /**
42
     * @var object[]
43
     */
44
    protected array $instances = [];
45
46
    /**
47
     * Container constructor.
48
     *
49
     * @param array $definitions Definitions to put into container.
50
     * @param ServiceProviderInterface[]|string[] $providers Service providers to get definitions from.
51 56
     *
52
     * @param ContainerInterface|null $rootContainer Root container to delegate lookup to in case definition
53
     * is not found in current container.
54
     * @throws InvalidConfigException
55
     */
56 56
    public function __construct(
57 55
        array $definitions = [],
58 54
        array $providers = [],
59 5
        ContainerInterface $rootContainer = null
60
    ) {
61 54
        $this->setMultiple($definitions);
62
        $this->addProviders($providers);
63
        if ($rootContainer !== null) {
64
            $this->delegateLookup($rootContainer);
65
        }
66
    }
67
68
    /**
69 24
     * Returns a value indicating whether the container has the definition of the specified name.
70
     * @param string $id class name, interface name or alias name
71 24
     * @return bool whether the container is able to provide instance of class specified.
72
     * @see set()
73
     */
74
    public function has($id): bool
75
    {
76
        return isset($this->definitions[$id]) || class_exists($id);
77
    }
78
79
    /**
80
     * Returns an instance by either interface name or alias.
81
     *
82
     * Same instance of the class will be returned each time this method is called.
83
     *
84
     * @param string|Reference $id The interface or an alias name that was previously registered.
85
     * @return object An instance of the requested interface.
86 50
     * @throws CircularReferenceException
87
     * @throws InvalidConfigException
88 50
     * @throws NotFoundException
89 50
     * @throws NotInstantiableException
90 50
     */
91
    public function get($id)
92
    {
93 41
        $id = $this->getId($id);
94
        $this->invalidateInstance($id);
95
        if (!isset($this->instances[$id])) {
96
            $this->instances[$id] = $this->build($id);
97
            $this->assignInstanceTag($id);
98
        }
99
100 5
        return $this->instances[$id];
101
    }
102 5
103 5
    /**
104
     * Delegate service lookup to another container.
105
     * @param ContainerInterface $container
106 5
     */
107 5
    protected function delegateLookup(ContainerInterface $container): void
108
    {
109
        if ($this->rootContainer === null) {
110
            $this->rootContainer = new CompositeContainer();
111
        }
112
113
        $this->rootContainer->attach($container);
0 ignored issues
show
Bug introduced by
The method attach() does not exist on Psr\Container\ContainerInterface. It seems like you code against a sub-type of Psr\Container\ContainerInterface such as Yiisoft\Di\CompositeContainer or Yiisoft\Di\CompositeContextContainer. ( Ignorable by Annotation )

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

113
        $this->rootContainer->/** @scrutinizer ignore-call */ 
114
                              attach($container);
Loading history...
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

113
        $this->rootContainer->/** @scrutinizer ignore-call */ 
114
                              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...
114
    }
115
116 48
    /**
117
     * Sets a definition to the container. Definition may be defined multiple ways.
118 48
     * @param string $id
119 47
     * @param mixed $definition
120 47
     * @throws InvalidConfigException
121 47
     * @see `Normalizer::normalize()`
122
     */
123
    protected function set(string $id, $definition): void
124
    {
125
        if ($this->hasInstanceTag($definition)) {
126
            $this->instanceTagCallbacks[$id] = $definition['__instanceTag'];
127
            $definition = $this->getDefinitionWithoutInstanceTag($definition);
128 56
        }
129
        $this->validateDefinition($definition);
130 56
        $this->instances[$id] = null;
131 45
        $this->definitions[$id] = $definition;
132
    }
133 55
134
    /**
135
     * Sets multiple definitions at once.
136
     * @param array $config definitions indexed by their ids
137
     * @throws InvalidConfigException
138
     */
139
    protected function setMultiple(array $config): void
140
    {
141
        foreach ($config as $id => $definition) {
142
            $this->set((string)$id, $definition);
143
        }
144
    }
145 50
146
    /**
147 50
     * Creates new instance by either interface name or alias.
148 7
     *
149 7
     * @param string $id The interface or an alias name that was previously registered.
150
     * @return object New built instance of the specified class.
151 7
     * @throws CircularReferenceException
152
     * @throws InvalidConfigException
153
     * @throws NotFoundException
154
     * @internal
155 50
     */
156 50
    private function build(string $id)
157 41
    {
158
        if (isset($this->building[$id])) {
159 41
            throw new CircularReferenceException(sprintf(
160
                'Circular reference to "%s" detected while building: %s',
161
                $id,
162 42
                implode(',', array_keys($this->building))
163
            ));
164 42
        }
165 1
166
        $this->building[$id] = 1;
167 42
        $object = $this->buildInternal($id);
168
        unset($this->building[$id]);
169 48
170
        return $object;
171 48
    }
172 4
173
    private function processDefinition($definition): void
174
    {
175 47
        if ($definition instanceof DeferredServiceProviderInterface) {
176 38
            $definition->register($this);
177
        }
178
    }
179 16
180 5
    private function hasInstanceTag($definition): bool
181
    {
182
        if (!is_array($definition) || !array_key_exists('__instanceTag', $definition)) {
183 11
            return false;
184 7
        }
185
186
        if (!is_callable($definition['__instanceTag'])) {
187 4
            throw new \RuntimeException("Definition array key '__instanceTag' must be callable.");
188 3
        }
189
190
        return true;
191 1
    }
192
193
    private function getDefinitionWithoutInstanceTag(array $definition)
194 50
    {
195
        unset($definition['__instanceTag']);
196 50
        if (isset($definition['__definition'])) {
197
            $definition = $definition['__definition'];
198
        }
199
200
        return $definition;
201
    }
202
203
    private function getInstanceTag(string $id): string
204
    {
205
        return ($this->instanceTagCallbacks[$id])($this);
206 50
    }
207
208 50
    private function assignInstanceTag(string $id): void
209 36
    {
210
        if (isset($this->instanceTagCallbacks[$id])) {
211 42
            $this->assignedInstanceTags[$id] = $this->getInstanceTag($id);
212 42
        }
213
    }
214 42
215
    private function invalidateInstance($id): void
216
    {
217
        if (isset($this->instanceTagCallbacks[$id])) {
218
            $currentTag = $this->getInstanceTag($id);
219
220
            if (isset($this->assignedInstanceTags[$id]) && $currentTag !== $this->assignedInstanceTags[$id]) {
221
                unset($this->instances[$id]);
222
            }
223
        }
224 36
    }
225
226 36
    private function validateDefinition($definition): void
227 34
    {
228
        if ($definition instanceof Reference || $definition instanceof DynamicReference) {
229 34
            return;
230
        }
231
232 3
        if (\is_string($definition)) {
233
            return;
234
        }
235 55
236
        if (\is_callable($definition)) {
237 55
            return;
238 4
        }
239
240 54
        if (\is_array($definition)) {
241
            return;
242
        }
243
244
        if (\is_object($definition)) {
245
            return;
246
        }
247
248
        throw new InvalidConfigException('Invalid definition:' . var_export($definition, true));
249
    }
250
251
    private function getId($id): string
252
    {
253 4
        return is_string($id) ? $id : $id->getId();
254
    }
255 4
256
    /**
257 3
     * @param string $id
258 1
     *
259 1
     * @return mixed|object
260
     * @throws InvalidConfigException
261
     * @throws NotFoundException
262 2
     */
263
    private function buildInternal(string $id)
264 3
    {
265
        if (!isset($this->definitions[$id])) {
266
            return $this->buildPrimitive($id);
267
        }
268
        $this->processDefinition($this->definitions[$id]);
269
        $definition = Normalizer::normalize($this->definitions[$id], $id);
270
271
        return $definition->resolve($this->rootContainer ?? $this);
272
    }
273
274 4
    /**
275
     * @param string $class
276 4
     *
277 3
     * @return mixed|object
278
     * @throws InvalidConfigException
279
     * @throws NotFoundException
280
     */
281
    private function buildPrimitive(string $class)
282
    {
283 3
        if (class_exists($class)) {
284
            $definition = new ArrayDefinition($class);
285
286
            return $definition->resolve($this->rootContainer ?? $this);
287
        }
288
289
        throw new NotFoundException("No definition for $class");
290
    }
291
292
    private function addProviders(array $providers): void
293
    {
294
        foreach ($providers as $provider) {
295
            $this->addProvider($provider);
296
        }
297
    }
298
299
    /**
300
     * Adds service provider to the container. Unless service provider is deferred
301
     * it would be immediately registered.
302
     *
303
     * @param string|array $providerDefinition
304
     *
305
     * @throws InvalidConfigException
306
     * @throws NotInstantiableException
307
     * @see ServiceProviderInterface
308
     * @see DeferredServiceProviderInterface
309
     */
310
    private function addProvider($providerDefinition): void
311
    {
312
        $provider = $this->buildProvider($providerDefinition);
313
314
        if ($provider instanceof DeferredServiceProviderInterface) {
315
            foreach ($provider->provides() as $id) {
316
                $this->definitions[$id] = $provider;
317
            }
318
        } else {
319
            $provider->register($this);
320
        }
321
    }
322
323
    /**
324
     * Builds service provider by definition.
325
     *
326
     * @param string|array $providerDefinition class name or definition of provider.
327
     * @return ServiceProviderInterface instance of service provider;
328
     *
329
     * @throws InvalidConfigException
330
     */
331
    private function buildProvider($providerDefinition): ServiceProviderInterface
332
    {
333
        $provider = Normalizer::normalize($providerDefinition)->resolve($this);
334
        if (!($provider instanceof ServiceProviderInterface)) {
335
            throw new InvalidConfigException(
336
                'Service provider should be an instance of ' . ServiceProviderInterface::class
337
            );
338
        }
339
340
        return $provider;
341
    }
342
}
343