1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
/** |
3
|
|
|
* Starlit App. |
4
|
|
|
* |
5
|
|
|
* @copyright Copyright (c) 2016 Starweb AB |
6
|
|
|
* @license BSD 3-Clause |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace Starlit\App\Container; |
10
|
|
|
|
11
|
|
|
use Psr\Container\ContainerInterface; |
12
|
|
|
|
13
|
|
|
class Container implements ContainerInterface |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* @var array |
17
|
|
|
*/ |
18
|
|
|
private $dicValues = []; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var array |
22
|
|
|
*/ |
23
|
|
|
private $aliases = []; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
private $dicObjects = []; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Set a DIC value. |
32
|
|
|
* |
33
|
|
|
* Wrap objects provided in a closure for lazy loading. |
34
|
|
|
* |
35
|
|
|
* @param string $key |
36
|
|
|
* @param mixed $value |
37
|
|
|
* @return ContainerInterface |
38
|
|
|
*/ |
39
|
25 |
|
public function set(string $key, $value): ContainerInterface |
40
|
|
|
{ |
41
|
25 |
|
if (!(\is_string($value) || \is_object($value))) { |
42
|
1 |
|
throw new \InvalidArgumentException('Value must be a class name, an object instance, or a callable'); |
43
|
|
|
} |
44
|
|
|
|
45
|
24 |
|
$this->dicValues[$key] = $value; |
46
|
24 |
|
unset($this->dicObjects[$key]); // In case an object instance was stored for sharing |
47
|
|
|
|
48
|
24 |
|
return $this; |
49
|
|
|
} |
50
|
|
|
|
51
|
1 |
|
public function unset(string $key): void |
52
|
|
|
{ |
53
|
|
|
unset( |
54
|
1 |
|
$this->dicValues[$key], |
55
|
1 |
|
$this->dicObjects[$key] |
56
|
|
|
); |
57
|
1 |
|
} |
58
|
|
|
|
59
|
23 |
|
public function alias(string $alias, string $key): ContainerInterface |
60
|
|
|
{ |
61
|
23 |
|
$this->aliases[$alias] = $key; |
62
|
|
|
|
63
|
23 |
|
return $this; |
64
|
|
|
} |
65
|
|
|
|
66
|
1 |
|
public function unalias(string $alias): void |
67
|
|
|
{ |
68
|
1 |
|
unset($this->aliases[$alias]); |
69
|
1 |
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @inheritdoc |
73
|
|
|
*/ |
74
|
4 |
|
public function has($key): bool |
75
|
|
|
{ |
76
|
4 |
|
if (isset($this->aliases[$key])) { |
77
|
1 |
|
$key = $this->aliases[$key]; |
78
|
|
|
} |
79
|
|
|
|
80
|
4 |
|
return isset($this->dicValues[$key]); |
81
|
|
|
} |
82
|
|
|
|
83
|
4 |
View Code Duplication |
public function hasInstance(string $key): bool |
|
|
|
|
84
|
|
|
{ |
85
|
4 |
|
if (isset($this->aliases[$key])) { |
86
|
1 |
|
$key = $this->aliases[$key]; |
87
|
|
|
} |
88
|
|
|
|
89
|
4 |
|
return isset($this->dicObjects[$key]); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @inheritdoc |
94
|
|
|
*/ |
95
|
36 |
|
public function get($key) |
96
|
|
|
{ |
97
|
36 |
|
if (isset($this->aliases[$key])) { |
98
|
1 |
|
$key = $this->aliases[$key]; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
// Get already instantiated object if it exist |
102
|
36 |
|
if (isset($this->dicObjects[$key])) { |
103
|
3 |
|
return $this->dicObjects[$key]; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
try { |
107
|
36 |
|
if (isset($this->dicValues[$key])) { |
108
|
25 |
|
$instance = $this->getValueInstance($key); |
109
|
|
|
} else { |
110
|
36 |
|
$instance = $this->resolveInstance($key); |
111
|
|
|
} |
112
|
4 |
|
} catch (\ReflectionException $e) { |
113
|
4 |
|
throw new NotFoundException(sprintf('Key "%s" could not be resolved.', $key)); |
114
|
|
|
} |
115
|
|
|
|
116
|
33 |
|
$this->dicObjects[$key] = $instance; |
117
|
|
|
|
118
|
33 |
|
return $this->dicObjects[$key]; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Get a new instance of a DIC object |
123
|
|
|
* |
124
|
|
|
* @param string $key |
125
|
|
|
* @return mixed |
126
|
|
|
*/ |
127
|
7 |
|
public function getNew(string $key) |
128
|
|
|
{ |
129
|
7 |
|
if (isset($this->aliases[$key])) { |
130
|
1 |
|
$key = $this->aliases[$key]; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
try { |
134
|
7 |
|
if (isset($this->dicValues[$key])) { |
135
|
4 |
|
return $this->getValueInstance($key); |
136
|
|
|
} |
137
|
3 |
|
return $this->resolveInstance($key); |
138
|
3 |
|
} catch (\ReflectionException $e) { |
139
|
3 |
|
throw new NotFoundException(sprintf('Key "%s" could not be resolved.', $key)); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
25 |
|
private function getValueInstance(string $key) |
144
|
|
|
{ |
145
|
25 |
|
$value = $this->dicValues[$key]; |
146
|
25 |
|
if (\is_object($value)) { |
147
|
|
|
// Is it an invokable? (closure/anonymous function) |
148
|
24 |
|
if (\method_exists($value, '__invoke')) { |
149
|
23 |
|
return $value($this); |
150
|
|
|
} |
151
|
7 |
|
return $value; |
152
|
|
|
} |
153
|
2 |
|
return $this->resolveInstance($value); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Destroy a DIC object instance. |
158
|
|
|
* |
159
|
|
|
* Will force a new object to be created on next call. |
160
|
|
|
* |
161
|
|
|
* @param string $key |
162
|
|
|
*/ |
163
|
3 |
View Code Duplication |
public function destroyInstance(string $key): void |
|
|
|
|
164
|
|
|
{ |
165
|
3 |
|
if (isset($this->aliases[$key])) { |
166
|
1 |
|
$key = $this->aliases[$key]; |
167
|
|
|
} |
168
|
|
|
|
169
|
3 |
|
unset($this->dicObjects[$key]); |
170
|
3 |
|
} |
171
|
|
|
|
172
|
2 |
|
public function destroyAllInstances(): void |
173
|
|
|
{ |
174
|
2 |
|
$this->dicObjects = []; |
175
|
|
|
|
176
|
|
|
// To make sure objects (like database connections) are destructed properly. PHP might not destruct objects |
177
|
|
|
// until the end of execution otherwise. |
178
|
2 |
|
\gc_collect_cycles(); |
179
|
2 |
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Magic method to get or set DIC values. |
183
|
|
|
* |
184
|
|
|
* @param string $name |
185
|
|
|
* @param array $arguments |
186
|
|
|
* @return mixed |
187
|
|
|
*/ |
188
|
8 |
|
public function __call(string $name, array $arguments) |
189
|
|
|
{ |
190
|
|
|
// getNew followed by an upper letter like getNewApple() |
191
|
8 |
|
if (\preg_match('/^getNew([A-Z].*)/', $name, $matches)) { |
192
|
1 |
|
$key = \lcfirst($matches[1]); |
193
|
|
|
|
194
|
1 |
|
return $this->getNew($key); |
195
|
7 |
|
} elseif (\strpos($name, 'get') === 0) { |
196
|
2 |
|
$key = \lcfirst(\substr($name, 3)); |
197
|
|
|
|
198
|
2 |
|
return $this->get($key); |
199
|
6 |
|
} elseif (\strpos($name, 'set') === 0) { |
200
|
4 |
|
$argumentCount = count($arguments); |
201
|
4 |
|
if ($argumentCount !== 1) { |
202
|
2 |
|
throw new \BadMethodCallException("Invalid argument count[{$argumentCount}] for application {$name}()"); |
203
|
|
|
} |
204
|
|
|
|
205
|
2 |
|
$key = \lcfirst(substr($name, 3)); |
206
|
|
|
|
207
|
2 |
|
return $this->set($key, $arguments[0]); |
208
|
|
|
} else { |
209
|
2 |
|
throw new \BadMethodCallException("No application method named {$name}()"); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Instantiate an object of named class, recursively resolving dependencies |
215
|
|
|
* |
216
|
|
|
* @param string $className Fully qualified class name |
217
|
|
|
* @return mixed |
218
|
|
|
* @throws \ReflectionException |
219
|
|
|
*/ |
220
|
16 |
|
private function resolveInstance(string $className) |
221
|
|
|
{ |
222
|
16 |
|
$class = new \ReflectionClass($className); |
223
|
|
|
|
224
|
13 |
|
if (!$class->isInstantiable()) { |
225
|
3 |
|
throw new \ReflectionException(sprintf('Class %s cannot be instantiated', $className)); |
226
|
|
|
} |
227
|
|
|
|
228
|
10 |
|
$parameterValues = []; |
229
|
10 |
|
if (($constructor = $class->getConstructor())) { |
230
|
1 |
|
$parameterValues = $this->resolveParameters( |
231
|
1 |
|
$constructor->getParameters() |
232
|
|
|
); |
233
|
|
|
} |
234
|
|
|
|
235
|
10 |
|
return $class->newInstanceArgs($parameterValues); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Recursively resolve function parameters using type hints |
240
|
|
|
* |
241
|
|
|
* @param \ReflectionParameter[] $parameters |
242
|
|
|
* @param array $predefinedValues |
243
|
|
|
* @return array |
244
|
|
|
* @throws \ReflectionException |
245
|
|
|
*/ |
246
|
12 |
|
public function resolveParameters(array $parameters, array $predefinedValues = []): array |
247
|
|
|
{ |
248
|
12 |
|
$values = []; |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* @var \ReflectionParameter $parameter |
252
|
|
|
*/ |
253
|
12 |
|
foreach ($parameters as $parameter) { |
254
|
12 |
|
if (\array_key_exists($parameter->getName(), $predefinedValues)) { |
255
|
8 |
|
if ($parameter->hasType() && $parameter->getType()->isBuiltin()) { |
256
|
6 |
|
\settype($predefinedValues[$parameter->getName()], $parameter->getType()->getName()); |
257
|
|
|
} |
258
|
8 |
|
$values[] = $predefinedValues[$parameter->getName()]; |
259
|
|
|
} else { |
260
|
11 |
|
if (($parameterClass = $parameter->getClass())) { |
261
|
|
|
try { |
262
|
10 |
|
$values[] = $this->get($parameterClass->getName()); |
263
|
|
|
} |
264
|
2 |
|
catch (NotFoundException $e) { // We're probably dealing with an unmapped interface here |
265
|
2 |
|
if ($parameter->isOptional()) { |
266
|
1 |
|
$values[] = $parameter->getDefaultValue(); |
267
|
|
|
} else { |
268
|
10 |
|
throw $e; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
} else { |
272
|
1 |
|
$values[] = $parameter->getDefaultValue(); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
11 |
|
return $values; |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.