Passed
Pull Request — master (#120)
by Dmitriy
13:32
created

Container::invalidateInstance()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 4
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 20
rs 10
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
    /**
40
     * @var object[]
41
     */
42
43
    private array $instances = [];
44
45
    private ?ContainerInterface $rootContainer = null;
46
47
    /**
48
     * Container constructor.
49
     *
50
     * @param array $definitions Definitions to put into container.
51 56
     * @param ServiceProviderInterface[]|string[] $providers Service providers to get definitions from.
52
     *
53
     * @param ContainerInterface|null $rootContainer Root container to delegate lookup to in case definition
54
     * is not found in current container.
55
     * @throws InvalidConfigException
56 56
     */
57 55
    public function __construct(
58 54
        array $definitions = [],
59 5
        array $providers = [],
60
        ContainerInterface $rootContainer = null
61 54
    ) {
62
        $this->setMultiple($definitions);
63
        $this->addProviders($providers);
64
        if ($rootContainer !== null) {
65
            $this->delegateLookup($rootContainer);
66
        }
67
    }
68
69 24
    /**
70
     * Returns a value indicating whether the container has the definition of the specified name.
71 24
     * @param string $id class name, interface name or alias name
72
     * @return bool whether the container is able to provide instance of class specified.
73
     * @see set()
74
     */
75
    public function has($id): bool
76
    {
77
        return isset($this->definitions[$id]) || class_exists($id);
78
    }
79
80
    /**
81
     * Returns an instance by either interface name or alias.
82
     *
83
     * Same instance of the class will be returned each time this method is called.
84
     *
85
     * @param string|Reference $id The interface or an alias name that was previously registered.
86 50
     * @return object An instance of the requested interface.
87
     * @throws CircularReferenceException
88 50
     * @throws InvalidConfigException
89 50
     * @throws NotFoundException
90 50
     * @throws NotInstantiableException
91
     */
92
    public function get($id)
93 41
    {
94
        $id = $this->getId($id);
95
        $this->invalidateInstance($id);
96
        if (!isset($this->instances[$id])) {
97
            $this->instances[$id] = $this->build($id);
98
            $this->assignInstanceTag($id);
99
        }
100 5
101
        return $this->instances[$id];
102 5
    }
103 5
104
    /**
105
     * Delegate service lookup to another container.
106 5
     * @param ContainerInterface $container
107 5
     */
108
    protected function delegateLookup(ContainerInterface $container): void
109
    {
110
        if ($this->rootContainer === null) {
111
            $this->rootContainer = new CompositeContainer();
112
        }
113
114
        $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

114
        $this->rootContainer->/** @scrutinizer ignore-call */ 
115
                              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

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