Passed
Pull Request — master (#144)
by Dmitriy
02:22
created

Container::processDefinition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 6
rs 10
c 0
b 0
f 0
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\ArrayDefinition;
11
use Yiisoft\Factory\Definitions\Normalizer;
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\Injector\Injector;
17
18
use function array_key_exists;
19
use function array_keys;
20
use function assert;
21
use function class_exists;
22
use function get_class;
23
use function implode;
24
use function is_object;
25
use function is_string;
26
27
/**
28
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
29
 */
30
final class Container extends AbstractContainerConfigurator implements ContainerInterface
31
{
32
    /**
33
     * @var array object definitions indexed by their types
34
     */
35
    private array $definitions = [];
36
    /**
37
     * @var array used to collect ids instantiated during build
38
     * to detect circular references
39
     */
40
    private array $building = [];
41
    /**
42
     * @var object[]
43
     */
44
    private array $instances = [];
45
    private ?CompositeContainer $rootContainer = null;
46
47
    /**
48
     * Container constructor.
49
     *
50
     * @param array $definitions Definitions to put into container.
51
     * @param ServiceProviderInterface[]|string[] $providers Service providers
52
     * to get definitions from.
53
     * @param ContainerInterface|null $rootContainer Root container to delegate
54
     * lookup to when resolving dependencies. If provided the current container
55
     * is no longer queried for dependencies.
56
     *
57
     * @throws InvalidConfigException
58
     */
59
    public function __construct(
60
        array $definitions = [],
61
        array $providers = [],
62
        ContainerInterface $rootContainer = null
63
    ) {
64
        $this->delegateLookup($rootContainer);
65
        $this->setDefaultDefinitions();
66
        $this->setMultiple($definitions);
67
        $this->addProviders($providers);
68
69
        // Prevent circular reference to ContainerInterface
70
        $this->get(ContainerInterface::class);
71
    }
72
73
    private function setDefaultDefinitions(): void
74
    {
75
        $container = $this->rootContainer ?? $this;
76
        $this->setMultiple([
77
            ContainerInterface::class => $container,
78
            Injector::class => new Injector($container),
79
        ]);
80
    }
81
82
    /**
83
     * Returns a value indicating whether the container has the definition of the specified name.
84
     *
85
     * @param string $id class name, interface name or alias name
86
     *
87
     * @return bool whether the container is able to provide instance of class specified.
88
     *
89
     * @see set()
90
     */
91
    public function has($id): bool
92
    {
93
        return isset($this->definitions[$id]) || class_exists($id);
94
    }
95
96
    /**
97
     * Returns an instance by either interface name or alias.
98
     *
99
     * Same instance of the class will be returned each time this method is called.
100
     *
101
     * @param string $id The interface or an alias name that was previously registered.
102
     *
103
     * @throws CircularReferenceException
104
     * @throws InvalidConfigException
105
     * @throws NotFoundException
106
     * @throws NotInstantiableException
107
     *
108
     * @return mixed|object An instance of the requested interface.
109
     *
110
     * @psalm-template T
111
     * @psalm-param string|class-string<T> $id
112
     * @psalm-return ($id is class-string ? T : mixed)
113
     */
114
    public function get($id)
115
    {
116
        if (!array_key_exists($id, $this->instances)) {
117
            $this->instances[$id] = $this->build($id);
118
        }
119
120
        return $this->instances[$id];
121
    }
122
123
    /**
124
     * Delegate service lookup to another container.
125
     *
126
     * @param ContainerInterface $container
127
     */
128
    protected function delegateLookup(?ContainerInterface $container): void
129
    {
130
        if ($container === null) {
131
            return;
132
        }
133
        if ($this->rootContainer === null) {
134
            $this->rootContainer = new CompositeContainer();
135
            $this->setDefaultDefinitions();
136
        }
137
138
        $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

138
        $this->rootContainer->/** @scrutinizer ignore-call */ 
139
                              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...
139
    }
140
141
    /**
142
     * Sets a definition to the container. Definition may be defined multiple ways.
143
     *
144
     * @param string $id
145
     * @param mixed $definition
146
     *
147
     * @throws InvalidConfigException
148
     *
149
     * @see `Normalizer::normalize()`
150
     */
151
    protected function set(string $id, $definition): void
152
    {
153
        Normalizer::validate($definition);
154
        unset($this->instances[$id]);
155
        $this->definitions[$id] = $definition;
156
    }
157
158
    /**
159
     * Sets multiple definitions at once.
160
     *
161
     * @param array $config definitions indexed by their ids
162
     *
163
     * @throws InvalidConfigException
164
     */
165
    protected function setMultiple(array $config): void
166
    {
167
        foreach ($config as $id => $definition) {
168
            if (!is_string($id)) {
169
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
170
            }
171
            $this->set($id, $definition);
172
        }
173
    }
174
175
    /**
176
     * Creates new instance by either interface name or alias.
177
     *
178
     * @param string $id The interface or an alias name that was previously registered.
179
     *
180
     * @throws CircularReferenceException
181
     * @throws InvalidConfigException
182
     * @throws NotFoundException
183
     *
184
     * @return mixed|object New built instance of the specified class.
185
     *
186
     * @internal
187
     */
188
    private function build(string $id)
189
    {
190
        if (isset($this->building[$id])) {
191
            if ($id === ContainerInterface::class) {
192
                return $this;
193
            }
194
            throw new CircularReferenceException(sprintf(
195
                'Circular reference to "%s" detected while building: %s.',
196
                $id,
197
                implode(',', array_keys($this->building))
198
            ));
199
        }
200
201
        $this->building[$id] = 1;
202
        try {
203
            $object = $this->buildInternal($id);
204
        } finally {
205
            unset($this->building[$id]);
206
        }
207
208
        return $object;
209
    }
210
211
    /**
212
     * @param mixed $definition
213
     */
214
    private function processDefinition($definition): void
215
    {
216
        if ($definition instanceof DeferredServiceProviderInterface) {
217
            $definition->register($this);
218
        }
219
    }
220
221
    /**
222
     * @param string $id
223
     *
224
     * @throws InvalidConfigException
225
     * @throws NotFoundException
226
     *
227
     * @return mixed|object
228
     */
229
    private function buildInternal(string $id)
230
    {
231
        if (!isset($this->definitions[$id])) {
232
            return $this->buildPrimitive($id);
233
        }
234
        $this->processDefinition($this->definitions[$id]);
235
        $definition = Normalizer::normalize($this->definitions[$id], $id);
236
237
        return $definition->resolve($this->rootContainer ?? $this);
238
    }
239
240
    /**
241
     * @param string $class
242
     *
243
     * @throws InvalidConfigException
244
     * @throws NotFoundException
245
     *
246
     * @return mixed|object
247
     */
248
    private function buildPrimitive(string $class)
249
    {
250
        if (class_exists($class)) {
251
            $definition = new ArrayDefinition($class);
252
253
            return $definition->resolve($this->rootContainer ?? $this);
254
        }
255
256
        throw new NotFoundException($class);
257
    }
258
259
    private function addProviders(array $providers): void
260
    {
261
        foreach ($providers as $provider) {
262
            $this->addProvider($provider);
263
        }
264
    }
265
266
    /**
267
     * Adds service provider to the container. Unless service provider is deferred
268
     * it would be immediately registered.
269
     *
270
     * @param mixed $providerDefinition
271
     *
272
     * @throws InvalidConfigException
273
     * @throws NotInstantiableException
274
     *
275
     * @see ServiceProviderInterface
276
     * @see DeferredServiceProviderInterface
277
     */
278
    private function addProvider($providerDefinition): void
279
    {
280
        $provider = $this->buildProvider($providerDefinition);
281
282
        if ($provider instanceof DeferredServiceProviderInterface) {
283
            foreach ($provider->provides() as $id) {
284
                $this->definitions[$id] = $provider;
285
            }
286
        } else {
287
            $provider->register($this);
288
        }
289
    }
290
291
    /**
292
     * Builds service provider by definition.
293
     *
294
     * @param mixed $providerDefinition class name or definition of provider.
295
     *
296
     * @throws InvalidConfigException
297
     *
298
     * @return ServiceProviderInterface instance of service provider;
299
     */
300
    private function buildProvider($providerDefinition): ServiceProviderInterface
301
    {
302
        $provider = Normalizer::normalize($providerDefinition)->resolve($this);
303
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
304
            sprintf(
305
                'Service provider should be an instance of %s. %s given.',
306
                ServiceProviderInterface::class,
307
                $this->getVariableType($provider)
308
            )
309
        ));
310
311
        return $provider;
312
    }
313
314
    /**
315
     * @param mixed $variable
316
     */
317
    private function getVariableType($variable): string
318
    {
319
        if (is_object($variable)) {
320
            return get_class($variable);
321
        }
322
323
        return gettype($variable);
324
    }
325
}
326