1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Jasny\Container; |
4
|
|
|
|
5
|
|
|
use Interop\Container\ContainerInterface as InteropContainer; |
6
|
|
|
use Psr\Container\ContainerInterface as Psr11Container; |
7
|
|
|
use Jasny\Container\Exception\NotFoundException; |
8
|
|
|
use Jasny\Container\Exception\NoSubContainerException; |
9
|
|
|
|
10
|
|
|
use function Jasny\expect_type; |
11
|
|
|
use Psr\Container\ContainerInterface; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* This class is a minimalist dependency injection container. |
15
|
|
|
* It has compatibility with container-interop's ContainerInterface and delegate-lookup feature. |
16
|
|
|
*/ |
17
|
|
|
class Container implements InteropContainer, AutowireContainerInterface |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* The delegate lookup. |
21
|
|
|
* |
22
|
|
|
* @var Psr11Container |
23
|
|
|
*/ |
24
|
|
|
protected $delegateLookupContainer; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The array of closures defining each entry of the container. |
28
|
|
|
* |
29
|
|
|
* @var \Closure[] |
30
|
|
|
*/ |
31
|
|
|
protected $callbacks; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* The array of entries once they have been instantiated. |
35
|
|
|
* |
36
|
|
|
* @var mixed[] |
37
|
|
|
*/ |
38
|
|
|
protected $instances = []; |
39
|
|
|
|
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Class constructor |
43
|
|
|
* |
44
|
|
|
* @param iterable<\Closure> $entries Entries must be passed as an array of anonymous functions. |
45
|
|
|
* @param Psr11Container $delegateLookupContainer Optional delegate lookup container. |
46
|
|
|
*/ |
47
|
|
|
public function __construct(iterable $entries, Psr11Container $delegateLookupContainer = null) |
48
|
12 |
|
{ |
49
|
|
|
$this->callbacks = $entries instanceof \Traversable ? iterator_to_array($entries) : $entries; |
|
|
|
|
50
|
12 |
|
$this->delegateLookupContainer = $delegateLookupContainer ?: $this; |
51
|
12 |
|
} |
52
|
12 |
|
|
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Get an instance from the container. |
56
|
|
|
* |
57
|
|
|
* @param string $identifier |
58
|
|
|
* @return mixed |
59
|
|
|
* @throws NotFoundException if entry isn't defined |
60
|
|
|
*/ |
61
|
11 |
|
public function get($identifier) |
62
|
|
|
{ |
63
|
11 |
|
expect_type($identifier, 'string'); |
64
|
|
|
|
65
|
11 |
|
if (array_key_exists($identifier, $this->instances)) { |
66
|
2 |
|
return $this->instances[$identifier]; |
67
|
|
|
} |
68
|
|
|
|
69
|
11 |
|
if (!isset($this->callbacks[$identifier])) { |
70
|
2 |
|
return $this->getSub($identifier); |
71
|
|
|
} |
72
|
|
|
|
73
|
11 |
|
$instance = $this->callbacks[$identifier]($this->delegateLookupContainer); |
74
|
2 |
|
$this->assertType($instance, $identifier); |
75
|
|
|
|
76
|
|
|
$this->instances[$identifier] = $instance; |
77
|
9 |
|
|
78
|
|
|
return $instance; |
79
|
9 |
|
} |
80
|
1 |
|
|
81
|
1 |
|
/** |
82
|
|
|
* Check if the container has an entry. |
83
|
|
|
* |
84
|
8 |
|
* @param string $identifier |
85
|
|
|
* @return bool |
86
|
8 |
|
*/ |
87
|
|
|
public function has($identifier): bool |
88
|
|
|
{ |
89
|
|
|
expect_type($identifier, 'string'); |
90
|
|
|
|
91
|
|
|
return isset($this->callbacks[$identifier]) || $this->hasSub($identifier); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Instantiate a new object, autowire dependencies. |
96
|
2 |
|
* |
97
|
|
|
* @param string $class |
98
|
2 |
|
* @param mixed ...$args |
99
|
|
|
* @return object |
100
|
2 |
|
*/ |
101
|
1 |
|
public function autowire(string $class, ...$args) |
102
|
|
|
{ |
103
|
|
|
return $this->get('Jasny\Autowire\AutowireInterface')->instantiate($class, ...$args); |
104
|
1 |
|
} |
105
|
|
|
|
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Check the type of the instance if the identifier is an interface or class name. |
109
|
|
|
* |
110
|
|
|
* @param mixed $instance |
111
|
|
|
* @param string $identifier |
112
|
|
|
* @throws \TypeError |
113
|
|
|
*/ |
114
|
2 |
|
protected function assertType($instance, string $identifier): void |
115
|
|
|
{ |
116
|
2 |
|
if ( |
117
|
|
|
ctype_upper($identifier[0]) && |
118
|
2 |
|
strpos($identifier, '.') === false && |
119
|
1 |
|
(class_exists($identifier) || interface_exists($identifier)) && |
120
|
|
|
!is_a($instance, $identifier) |
121
|
|
|
) { |
122
|
2 |
|
$type = (is_object($instance) ? get_class($instance) . ' ' : '') . gettype($instance); |
123
|
|
|
throw new \TypeError("Entry is a $type, which does not implement $identifier"); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get an instance from a subcontainer. |
130
|
|
|
* |
131
|
|
|
* @param string $identifier |
132
|
1 |
|
* @return mixed |
133
|
|
|
* @throws NotFoundException |
134
|
1 |
|
*/ |
135
|
1 |
|
protected function getSub(string $identifier) |
136
|
|
|
{ |
137
|
|
|
[$subcontainer, $containerId, $subId] = $this->findSubcontainer($identifier); |
138
|
|
|
|
139
|
|
|
if (!isset($subcontainer)) { |
140
|
|
|
throw new NotFoundException("Entry \"$identifier\" is not defined."); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
if (!$subcontainer instanceof ContainerInterface) { |
144
|
|
|
throw new NoSubContainerException("Entry \"$containerId\" is not a PSR-11 compatible container"); |
145
|
|
|
} |
146
|
3 |
|
|
147
|
|
|
|
148
|
3 |
|
return $subcontainer->get($subId); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Get an instance from a subcontainer. |
153
|
|
|
* |
154
|
|
|
* @param string $identifier |
155
|
|
|
* @return bool |
156
|
|
|
*/ |
157
|
|
|
protected function hasSub(string $identifier): bool |
158
|
|
|
{ |
159
|
|
|
[$subcontainer, , $subId] = $this->findSubcontainer($identifier); |
160
|
|
|
|
161
|
|
|
return isset($subcontainer) && $subcontainer instanceof ContainerInterface && $subcontainer->has($subId); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Find an subcontainer iterating through the identifier parts. |
166
|
|
|
* |
167
|
|
|
* @param string $identifier |
168
|
|
|
* @return array [subcontainer, subidentifier] |
169
|
|
|
*/ |
170
|
|
|
protected function findSubcontainer(string $identifier): array |
171
|
|
|
{ |
172
|
|
|
$containerId = null; |
173
|
|
|
$subcontainer = null; |
174
|
|
|
$parts = explode('.', $identifier); |
175
|
|
|
$subParts = []; |
176
|
|
|
|
177
|
|
|
while ($parts !== []) { |
178
|
|
|
array_unshift($subParts, array_pop($parts)); |
179
|
|
|
$containerId = join('.', $parts); |
180
|
|
|
|
181
|
|
|
if (isset($this->callbacks[$containerId])) { |
182
|
|
|
$subcontainer = $this->get($containerId); |
183
|
|
|
break; |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
return isset($subcontainer) ? [$subcontainer, $containerId, join('.', $subParts)] : [null, null, null]; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.