Completed
Push — master ( 9d637e...d4b5b7 )
by Anton
01:30
created

Container.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php declare(strict_types=1);
2
3
namespace Phact\Container;
4
5
use Phact\Container\Builder\Builder;
6
use Phact\Container\Builder\BuilderInterface;
7
use Phact\Container\Definition\Definition;
8
use Phact\Container\Definition\DefinitionInterface;
9
use Phact\Container\Delegate\Delegate;
10
use Phact\Container\Exceptions\CircularException;
11
use Phact\Container\Exceptions\DuplicateNameException;
12
use Phact\Container\Exceptions\NotFoundException;
13
use Phact\Container\Inflection\InflectionInterface;
14
use Psr\Container\ContainerInterface as PsrContainerInterface;
15
16
class Container implements ContainerInterface
17
{
18
    /**
19
     * @var array
20
     */
21
    protected $scalars = [];
22
23
    /**
24
     * @var Definition[]
25
     */
26
    protected $definitions = [];
27
28
    /**
29
     * @var array
30
     */
31
    protected $references = [];
32
33
    /**
34
     * @var array
35
     */
36
    protected $aliases = [];
37
38
    /**
39
     * Resolved shared instances
40
     *
41
     * @var array
42
     */
43
    protected $shared = [];
44
45
    /**
46
     * @var InflectionInterface[]
47
     */
48
    protected $inflections = [];
49
50
    /**
51
     * @var Delegate[]
52
     */
53
    protected $delegates = [];
54
55
    /**
56
     * Now loading services
57
     * @var array
58
     */
59
    protected $loading = [];
60
61
    /**
62
     * @var bool
63
     */
64
    protected $autoWire = true;
65
66
    /**
67
     * @var bool
68
     */
69
    protected $analyzeReferences = true;
70
71
    /**
72
     * @var BuilderInterface
73
     */
74
    protected $builder;
75
76 23
    public function __construct(?BuilderInterface $builder = null)
77
    {
78 23
        if (!$builder) {
79 20
            $builder = new Builder();
80
        }
81 23
        $this->builder = $builder;
82 23
        $this->builder->setContainer($this);
83 23
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88 1
    public function addDefinitionClass(string $name, string $class): DefinitionInterface
89
    {
90 1
        return $this->addDefinition($name, new Definition($class));
91
    }
92
93
    /**
94
     * {@inheritDoc}
95
     */
96 14
    public function addDefinition(string $name, DefinitionInterface $definition): DefinitionInterface
97
    {
98 14
        if (isset($this->definitions[$name])) {
99 1
            throw new DuplicateNameException("Definition with name {$name} already exists.");
100
        }
101 14
        $this->definitions[$name] = $definition;
102 14
        if ($this->analyzeReferences) {
103 13
            $this->addReferences($name, $definition->getClass());
104
        }
105 14
        $this->addAliases($name, $definition->getAliases());
106 14
        return $definition;
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112 2
    public function addScalar(string $name, $value): void
113
    {
114 2
        $this->scalars[$name] = $value;
115 2
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120 13
    protected function addReferences(string $name, string $class): void
121
    {
122 13
        $this->addReference($name, $class);
123 13
        $interfaces = class_implements($class);
124 13
        foreach ($interfaces as $interface) {
125 3
            $this->addReference($name, $interface);
126
        }
127 13
        $parents = class_parents($class);
128 13
        foreach ($parents as $parent) {
129 3
            $this->addReference($name, $parent);
130
        }
131 13
    }
132
133
    /**
134
     * {@inheritDoc}
135
     */
136 13
    public function addReference(string $name, string $class): void
137
    {
138 13
        if (!isset($this->references[$class])) {
139 13
            $this->references[$class] = [];
140
        }
141 13
        $this->references[$class][] = $name;
142 13
    }
143
144
    /**
145
     * {@inheritDoc}
146
     */
147 14
    public function addAliases(string $name, array $aliases = []): void
148
    {
149 14
        foreach ($aliases as $alias) {
150 2
            $this->addAlias($name, $alias);
151
        }
152 14
    }
153
154
    /**
155
     * {@inheritDoc}
156
     */
157 2
    public function addAlias(string $name, string $alias): void
158
    {
159 2
        if (!isset($this->aliases[$alias])) {
160 2
            $this->aliases[$alias] = [];
161
        }
162 2
        $this->aliases[$alias][] = $name;
163 2
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 2
    public function addInflection(InflectionInterface $inflection): InflectionInterface
169
    {
170 2
        $this->inflections[] = $inflection;
171 2
        return $inflection;
172
    }
173
174
    /**
175
     * Resolve definition by provided id from definitions, aliases and references
176
     *
177
     * @param string $id
178
     * @return object
179
     * @throws CircularException
180
     */
181 8
    protected function resolveDefinitionById(string $id): object
182
    {
183 8
        if (!isset($this->definitions[$id])) {
184 4
            if (isset($this->aliases[$id])) {
185 1
                $id = reset($this->aliases[$id]);
186
            }
187 4
            if ($this->analyzeReferences && isset($this->references[$id])) {
188 3
                $id = reset($this->references[$id]);
189
            }
190
        }
191 8
        return $this->resolveDefinition($this->definitions[$id], $id);
192
    }
193
194
    /**
195
     * Create object by DefinitionInterface object or retrieve it from shared
196
     *
197
     * @param DefinitionInterface $definition
198
     * @param string|null $id
199
     * @return object
200
     * @throws CircularException
201
     */
202 9
    protected function resolveDefinition(DefinitionInterface $definition, ?string $id = null): object
203
    {
204 9
        $this->setLoading($id);
205 9
        $object = $this->builder->construct($definition);
206 8
        if ($id && $definition->isShared()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
207 7
            $this->shared[$id] = $object;
208
        }
209 8
        $object = $this->builder->configure($object, $definition);
210 8
        $object = $this->inflect($object);
211 8
        $this->unsetLoading($id);
212 8
        return $object;
213
    }
214
215
    /**
216
     * Set that certain service are loading and checks circular exception
217
     *
218
     * @param string|null $id
219
     * @throws CircularException
220
     */
221 9
    protected function setLoading(?string $id): void
222
    {
223 9
        if ($id === null) {
224 2
            return;
225
        }
226 8
        if (isset($this->loading[$id])) {
227 1
            $loadingServices = implode(', ', array_keys($this->loading));
228 1
            throw new CircularException(sprintf("Circular dependency detected with services - %s", $loadingServices));
229
        }
230 8
        $this->loading[$id] = true;
231 8
    }
232
233
    /**
234
     * Unset that certain service are loading
235
     *
236
     * @param string|null $id
237
     */
238 8
    protected function unsetLoading(?string $id): void
239
    {
240 8
        if ($id === null) {
241 2
            return;
242
        }
243 7
        unset($this->loading[$id]);
244 7
    }
245
246
    /**
247
     * Check that definition exists as definition or alias or reference
248
     *
249
     * @param string $id
250
     * @return bool
251
     */
252 19
    protected function hasDefinition(string $id): bool
253
    {
254
        return (
255 19
            isset($this->definitions[$id]) ||
256 13
            isset($this->aliases[$id]) ||
257 19
            ($this->analyzeReferences && isset($this->references[$id]))
258
        );
259
    }
260
261
    /**
262
     * Apply all necessary inflections to object
263
     *
264
     * @param object $object
265
     * @return object
266
     */
267 9
    protected function inflect(object $object): object
268
    {
269 9
        foreach ($this->inflections as $inflection) {
270 2
            if ($inflection->canBeAppliedTo($object)) {
271 1
                $object = $this->builder->inflect($object, $inflection);
272
            }
273
        }
274 9
        return $object;
275
    }
276
277
    /**
278
     * @inheritDoc
279
     */
280 12
    public function get($id)
281
    {
282 12
        if (isset($this->shared[$id])) {
283 2
            return $this->shared[$id];
284
        }
285
286 12
        if (isset($this->scalars[$id])) {
287 1
            return $this->scalars[$id];
288
        }
289
290 11
        if ($this->hasDefinition($id)) {
291 8
            return $this->resolveDefinitionById($id);
292
        }
293
294 4
        if ($this->autoWire && class_exists($id)) {
295 2
            return $this->resolveDefinition(
296 2
                (new Definition($id))->setShared(true)
297
            );
298
        }
299
300 2
        if ($resolved = $this->getFromDelegates($id)) {
301 1
            return $resolved;
302
        }
303
304 1
        throw new NotFoundException("Could not resolve element by id - {$id}");
305
    }
306
307
    /**
308
     * Try to find entry in delegates
309
     *
310
     * @param $id
311
     * @return mixed|object|null
312
     */
313 2
    public function getFromDelegates($id)
314
    {
315 2
        foreach ($this->delegates as $delegate) {
316 1
            if ($delegate->getContainer()->has($id)) {
317 1
                $resolved = $delegate->getContainer()->get($id);
318 1
                if ($delegate->isApplyInflection()) {
319 1
                    $resolved = $this->inflect($resolved);
320
                }
321 1
                return $resolved;
322
            }
323
        }
324 1
        return null;
325
    }
326
327
    /**
328
     * @inheritDoc
329
     */
330 14
    public function has($id): bool
331
    {
332 14
        if (isset($this->shared[$id])) {
333 1
            return true;
334
        }
335
336 13
        if (isset($this->scalars[$id])) {
337 2
            return true;
338
        }
339
340 11
        if ($this->hasDefinition($id)) {
341 6
            return true;
342
        }
343
344 5
        if ($this->autoWire && class_exists($id)) {
345 1
            return true;
346
        }
347
348 4
        foreach ($this->delegates as $delegate) {
349 1
            if ($delegate->getContainer()->has($id)) {
350 1
                return true;
351
            }
352
        }
353
354 3
        return false;
355
    }
356
357
    /**
358
     * @inheritDoc
359
     */
360 1
    public function invoke(callable $callable, array $arguments = [])
361
    {
362 1
        return $this->builder->invoke($callable, $arguments);
363
    }
364
365
    /**
366
     * @inheritDoc
367
     */
368 2
    public function addDelegate(PsrContainerInterface $container, bool $applyInflection = true): void
369
    {
370 2
        $this->delegates[] = new Delegate($container, $applyInflection);
371 2
    }
372
373
    /**
374
     * Set that all dependencies will be analyzed for constructors, callable objects and methods
375
     *
376
     * @param bool $autoWire
377
     * @return $this
378
     */
379 1
    public function setAutoWire(bool $autoWire): self
380
    {
381 1
        $this->autoWire = $autoWire;
382 1
        return $this;
383
    }
384
385
    /**
386
     * Set analyze all added classes with class_parents and class_implements and create references.
387
     * When you add Child class to container and try get object by Parent class you will get Child class object
388
     * that described in container.
389
     *
390
     * @param bool $analyzeReferences
391
     * @return $this
392
     */
393 1
    public function setAnalyzeReferences(bool $analyzeReferences): self
394
    {
395 1
        $this->analyzeReferences = $analyzeReferences;
396 1
        return $this;
397
    }
398
}