1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace BrainExe\Core\Annotations\Builder; |
4
|
|
|
|
5
|
|
|
use BrainExe\Core\Annotations\Inject; |
6
|
|
|
use BrainExe\Core\Annotations\Service; |
7
|
|
|
use BrainExe\Core\Annotations\ServiceIdGenerator; |
8
|
|
|
use Doctrine\Common\Annotations\Reader; |
9
|
|
|
use InvalidArgumentException; |
10
|
|
|
use ReflectionClass; |
11
|
|
|
use ReflectionMethod; |
12
|
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder; |
13
|
|
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
14
|
|
|
use Symfony\Component\DependencyInjection\Definition; |
15
|
|
|
use Symfony\Component\DependencyInjection\Reference; |
16
|
|
|
use Symfony\Component\ExpressionLanguage\Expression; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @author Matthias Dötsch <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
class ServiceDefinition |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* @var Reader |
25
|
|
|
*/ |
26
|
|
|
protected $reader; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var ServiceIdGenerator |
30
|
|
|
*/ |
31
|
|
|
private $serviceIdGenerator; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var ContainerBuilder |
35
|
|
|
*/ |
36
|
|
|
protected $container; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @param ContainerBuilder $container |
40
|
|
|
* @param Reader $reader |
41
|
|
|
*/ |
42
|
1 |
|
public function __construct(ContainerBuilder $container, Reader $reader) |
43
|
|
|
{ |
44
|
1 |
|
$this->reader = $reader; |
45
|
1 |
|
$this->serviceIdGenerator = new ServiceIdGenerator(); |
46
|
1 |
|
$this->container = $container; |
47
|
1 |
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @param ReflectionClass $reflectionClass |
51
|
|
|
* @param Service $annotation |
52
|
|
|
* @param Definition $definition |
53
|
|
|
* @return array |
54
|
|
|
*/ |
55
|
1 |
|
public function build(ReflectionClass $reflectionClass, Service $annotation, Definition $definition) |
56
|
|
|
{ |
57
|
1 |
|
$constructor = $reflectionClass->getConstructor(); |
58
|
1 |
|
if (null !== $constructor) { |
59
|
1 |
|
$this->processConstructor($constructor, $definition); |
60
|
|
|
} |
61
|
|
|
|
62
|
1 |
|
$this->processService($annotation, $definition); |
63
|
1 |
|
$this->processMethods($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC), $definition); |
64
|
1 |
|
$this->processTags($reflectionClass, $annotation, $definition); |
65
|
|
|
|
66
|
1 |
|
$serviceId = $annotation->value ?: $annotation->name; |
67
|
1 |
|
if (empty($serviceId)) { |
68
|
1 |
|
$serviceId = $this->serviceIdGenerator->generate($reflectionClass->getName()); |
69
|
|
|
} |
70
|
|
|
|
71
|
1 |
|
return [$this->setupDefinition($definition, $serviceId) ?? $serviceId, $definition]; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param Definition $definition |
76
|
|
|
* @return string null |
77
|
|
|
*/ |
78
|
1 |
|
public function setupDefinition(Definition $definition, string $serviceId) |
79
|
|
|
{ |
80
|
1 |
|
return null; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @param string|string[] $value |
85
|
|
|
* @return Reference[]|Reference|Expression |
86
|
|
|
*/ |
87
|
1 |
|
private function resolveServices($value) |
88
|
|
|
{ |
89
|
1 |
|
if (is_array($value)) { |
90
|
|
|
return array_map([$this, 'resolveServices'], $value); |
91
|
1 |
|
} elseif ('=' === $value[0]) { |
92
|
1 |
|
return new Expression(mb_substr($value, 1)); |
93
|
1 |
|
} elseif ('@' === $value[0]) { |
94
|
1 |
|
return $this->getValueReference($value); |
95
|
|
|
} |
96
|
|
|
|
97
|
1 |
|
return $value; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @param ReflectionMethod $constructor |
102
|
|
|
* @param Definition $definition |
103
|
|
|
*/ |
104
|
1 |
|
protected function processConstructor(ReflectionMethod $constructor, Definition $definition) |
105
|
|
|
{ |
106
|
1 |
|
if ($annotation = $this->reader->getMethodAnnotation($constructor, Inject::class)) { |
107
|
|
|
/** @var Inject $annotation */ |
108
|
1 |
|
$arguments = $this->extractArguments( |
109
|
|
|
$constructor, |
110
|
|
|
$annotation, |
111
|
1 |
|
$definition->getArguments() |
112
|
|
|
); |
113
|
|
|
|
114
|
1 |
|
$definition->setArguments($arguments); |
115
|
1 |
|
} elseif ($constructor->getNumberOfParameters() > 0) { |
116
|
1 |
|
$definition->setArguments( |
117
|
1 |
|
$this->resolveArguments($constructor) |
118
|
|
|
); |
119
|
|
|
|
120
|
1 |
|
$definition->setAutowired(true); |
121
|
|
|
} |
122
|
1 |
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @param ReflectionMethod[] $methods |
126
|
|
|
* @param Definition $definition |
127
|
|
|
*/ |
128
|
1 |
|
protected function processMethods(array $methods, Definition $definition) |
129
|
|
|
{ |
130
|
1 |
|
foreach ($methods as $method) { |
131
|
1 |
|
if ($method->isConstructor()) { |
132
|
1 |
|
continue; |
133
|
|
|
} |
134
|
1 |
|
$this->processMethod($definition, $method); |
135
|
|
|
} |
136
|
1 |
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @param ReflectionMethod $method |
140
|
|
|
* @param Inject $annotation |
141
|
|
|
* @param array $arguments |
142
|
|
|
* @return array |
143
|
|
|
*/ |
144
|
1 |
|
private function extractArguments( |
145
|
|
|
ReflectionMethod $method, |
146
|
|
|
Inject $annotation, |
147
|
|
|
array $arguments = [] |
148
|
|
|
) : array { |
149
|
1 |
|
$values = []; |
150
|
1 |
|
if (is_string($annotation->value)) { |
151
|
1 |
|
$values = [$annotation->value]; |
152
|
1 |
|
} elseif (is_array($annotation->value)) { |
153
|
1 |
|
$values = $annotation->value; |
154
|
|
|
} |
155
|
|
|
|
156
|
1 |
|
return $this->resolveArguments($method, $values, $arguments); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @param ReflectionMethod $method |
161
|
|
|
* @param array $values |
162
|
|
|
* @param array $arguments |
163
|
|
|
* @return array |
164
|
|
|
*/ |
165
|
1 |
|
private function resolveArguments( |
166
|
|
|
ReflectionMethod $method, |
167
|
|
|
array $values = [], |
168
|
|
|
array $arguments = [] |
169
|
|
|
) : array { |
170
|
1 |
|
foreach ($method->getParameters() as $index => $parameter) { |
171
|
1 |
|
$name = $parameter->getName(); |
172
|
1 |
|
if (!empty($values[$name])) { |
173
|
1 |
|
$arguments[$index] = $this->resolveServices($values[$name]); |
174
|
1 |
|
} elseif (isset($values[$index])) { |
175
|
1 |
|
$arguments[$index] = $this->resolveServices($values[$index]); |
176
|
1 |
|
} elseif (!isset($arguments[$index])) { |
177
|
1 |
|
$parameterClass = $parameter->getClass(); |
178
|
1 |
|
if ($parameterClass) { |
179
|
1 |
|
$arguments[$index] = new Reference($parameterClass->getName()); |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
|
184
|
1 |
|
return $arguments; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* @param Service $annotation |
189
|
|
|
* @param Definition $definition |
190
|
|
|
*/ |
191
|
1 |
|
private function processService(Service $annotation, Definition $definition) |
192
|
|
|
{ |
193
|
1 |
|
$definition->setAutowired(true); |
194
|
1 |
|
$definition->setPublic($annotation->public); |
195
|
1 |
|
$definition->setLazy($annotation->lazy); |
196
|
1 |
|
$definition->setShared($annotation->shared); |
197
|
1 |
|
$definition->setSynthetic($annotation->synthetic); |
198
|
1 |
|
$definition->setAbstract($annotation->abstract); |
199
|
1 |
|
$this->processConfigurator($annotation, $definition); |
200
|
|
|
|
201
|
1 |
|
if (isset($annotation->factory)) { |
202
|
|
|
$definition->setFactory($annotation->factory); |
203
|
|
|
} |
204
|
1 |
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param ReflectionClass $reflectionClass |
208
|
|
|
* @param Service $annotation |
209
|
|
|
* @param Definition $definition |
210
|
|
|
*/ |
211
|
1 |
|
private function processTags( |
212
|
|
|
ReflectionClass $reflectionClass, |
213
|
|
|
Service $annotation, |
214
|
|
|
Definition $definition |
215
|
|
|
) { |
216
|
1 |
|
if (empty($annotation->tags)) { |
217
|
1 |
|
return; |
218
|
|
|
} |
219
|
|
|
|
220
|
1 |
|
foreach ($annotation->tags as $tag) { |
221
|
1 |
|
if (!isset($tag['name'])) { |
222
|
|
|
throw new InvalidArgumentException( |
223
|
|
|
sprintf( |
224
|
|
|
'A "tags" entry is missing a "name" key must be an array for class "%s" in %s.', |
225
|
|
|
$reflectionClass->getName(), |
226
|
|
|
$reflectionClass->getFileName() |
227
|
|
|
) |
228
|
|
|
); |
229
|
|
|
} |
230
|
1 |
|
$name = $tag['name']; |
231
|
1 |
|
unset($tag['name']); |
232
|
|
|
|
233
|
1 |
|
$definition->addTag($name, $tag); |
234
|
|
|
} |
235
|
1 |
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @param string $value |
239
|
|
|
* @return Reference |
240
|
|
|
*/ |
241
|
1 |
|
private function getValueReference($value) : Reference |
242
|
|
|
{ |
243
|
1 |
|
if (0 === strpos($value, '@?')) { |
244
|
|
|
$value = substr($value, 2); |
245
|
|
|
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; |
246
|
|
|
} else { |
247
|
1 |
|
$value = substr($value, 1); |
248
|
1 |
|
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
// mark reference as strict |
252
|
1 |
|
if ('=' === substr($value, -1)) { |
253
|
|
|
$value = substr($value, 0, -1); |
254
|
|
|
$strict = false; |
255
|
|
|
} else { |
256
|
1 |
|
$strict = true; |
257
|
|
|
} |
258
|
|
|
|
259
|
1 |
|
return new Reference($value, $invalidBehavior, $strict); |
|
|
|
|
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* @param Service $annotation |
264
|
|
|
* @param Definition $definition |
265
|
|
|
*/ |
266
|
1 |
|
private function processConfigurator(Service $annotation, Definition $definition) |
267
|
|
|
{ |
268
|
1 |
|
if (!isset($annotation->configurator)) { |
269
|
1 |
|
return; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if (is_string($annotation->configurator)) { |
273
|
|
|
$definition->setConfigurator($annotation->configurator); |
274
|
|
|
} else { |
275
|
|
|
$definition->setConfigurator([ |
276
|
|
|
$this->resolveServices($annotation->configurator[0]), |
277
|
|
|
$annotation->configurator[1] |
278
|
|
|
]); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* @param Definition $definition |
284
|
|
|
* @param ReflectionMethod $method |
285
|
|
|
*/ |
286
|
1 |
|
protected function processMethod(Definition $definition, ReflectionMethod $method) |
287
|
|
|
{ |
288
|
|
|
/** @var Inject $annotation */ |
289
|
1 |
|
$annotation = $this->reader->getMethodAnnotation($method, Inject::class); |
290
|
1 |
|
if ($annotation) { |
291
|
1 |
|
$arguments = $this->extractArguments( |
292
|
|
|
$method, |
293
|
|
|
$annotation |
294
|
|
|
); |
295
|
1 |
|
$definition->addMethodCall($method->getName(), $arguments); |
296
|
|
|
} |
297
|
1 |
|
} |
298
|
|
|
} |
299
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.