1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace TheCodingMachine\Funky; |
5
|
|
|
|
6
|
|
|
use Doctrine\Common\Annotations\AnnotationReader; |
7
|
|
|
use Doctrine\Common\Annotations\AnnotationRegistry; |
8
|
|
|
use Interop\Container\ServiceProviderInterface; |
9
|
|
|
use ReflectionClass; |
10
|
|
|
use TheCodingMachine\Funky\Annotations\Extension; |
11
|
|
|
use TheCodingMachine\Funky\Annotations\Factory; |
12
|
|
|
use TheCodingMachine\Funky\Annotations\Tag; |
13
|
|
|
use TheCodingMachine\Funky\Utils\FileSystem; |
14
|
|
|
|
15
|
|
|
class ServiceProvider implements ServiceProviderInterface |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* @var ReflectionClass |
19
|
|
|
*/ |
20
|
|
|
private $refClass; |
21
|
|
|
/** |
22
|
|
|
* @var string |
23
|
|
|
*/ |
24
|
|
|
private $className; |
25
|
|
|
|
26
|
|
|
private static $annotationReader; |
27
|
|
|
|
28
|
|
|
|
29
|
|
|
private static function getAnnotationReader() : AnnotationReader |
30
|
|
|
{ |
31
|
|
|
if (self::$annotationReader === null) { |
32
|
|
|
AnnotationRegistry::registerLoader('class_exists'); |
|
|
|
|
33
|
|
|
|
34
|
|
|
|
35
|
|
|
self::$annotationReader = new AnnotationReader(); |
36
|
|
|
} |
37
|
|
|
return self::$annotationReader; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @return FactoryDefinition[] |
42
|
|
|
*/ |
43
|
|
|
private function getFactoryDefinitions(): array |
44
|
|
|
{ |
45
|
|
|
$refClass = $this->getRefClass(); |
46
|
|
|
$factories = []; |
47
|
|
|
|
48
|
|
|
foreach ($refClass->getMethods() as $method) { |
49
|
|
|
$factoryAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Factory::class); |
50
|
|
|
if ($factoryAnnotation) { |
51
|
|
|
$factories[] = new FactoryDefinition($method, $factoryAnnotation); |
52
|
|
|
} |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
return $factories; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @return ExtensionDefinition[] |
60
|
|
|
*/ |
61
|
|
|
private function getExtensionDefinitions(): array |
62
|
|
|
{ |
63
|
|
|
$refClass = $this->getRefClass(); |
64
|
|
|
$extensions = []; |
65
|
|
|
|
66
|
|
|
foreach ($refClass->getMethods() as $method) { |
67
|
|
|
$extensionAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Extension::class); |
68
|
|
|
if ($extensionAnnotation) { |
69
|
|
|
$extensions[] = new ExtensionDefinition($method, $extensionAnnotation); |
70
|
|
|
} |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
return $extensions; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
private function init(): void |
77
|
|
|
{ |
78
|
|
|
if ($this->className === null) { |
79
|
|
|
[$className, $fileName] = $this->getFileAndClassName(); |
80
|
|
|
if (!file_exists($fileName) || filemtime($this->getRefClass()->getFileName()) > filemtime($fileName)) { |
81
|
|
|
$this->dumpHelper(); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
require_once $fileName; |
85
|
|
|
|
86
|
|
|
$this->className = $className; |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Returns a list of all container entries registered by this service provider. |
92
|
|
|
* |
93
|
|
|
* - the key is the entry name |
94
|
|
|
* - the value is a callable that will return the entry, aka the **factory** |
95
|
|
|
* |
96
|
|
|
* Factories have the following signature: |
97
|
|
|
* function(\Psr\Container\ContainerInterface $container) |
98
|
|
|
* |
99
|
|
|
* @return callable[] |
100
|
|
|
*/ |
101
|
|
|
public function getFactories() |
102
|
|
|
{ |
103
|
|
|
$this->init(); |
104
|
|
|
$factoriesCallable = $this->className.'::getFactories'; |
105
|
|
|
return $factoriesCallable(); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Returns a list of all container entries extended by this service provider. |
110
|
|
|
* |
111
|
|
|
* - the key is the entry name |
112
|
|
|
* - the value is a callable that will return the modified entry |
113
|
|
|
* |
114
|
|
|
* Callables have the following signature: |
115
|
|
|
* function(Psr\Container\ContainerInterface $container, $previous) |
116
|
|
|
* or function(Psr\Container\ContainerInterface $container, $previous = null) |
117
|
|
|
* |
118
|
|
|
* About factories parameters: |
119
|
|
|
* |
120
|
|
|
* - the container (instance of `Psr\Container\ContainerInterface`) |
121
|
|
|
* - the entry to be extended. If the entry to be extended does not exist and the parameter is nullable, |
122
|
|
|
* `null` will be passed. |
123
|
|
|
* |
124
|
|
|
* @return callable[] |
125
|
|
|
*/ |
126
|
|
|
public function getExtensions() |
127
|
|
|
{ |
128
|
|
|
$this->init(); |
129
|
|
|
$extensionsCallable = $this->className.'::getExtensions'; |
130
|
|
|
return $extensionsCallable(); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Writes the helper class and returns the file path. |
135
|
|
|
* |
136
|
|
|
* @return string |
137
|
|
|
* @throws \TheCodingMachine\Funky\IoException |
138
|
|
|
*/ |
139
|
|
|
public function dumpHelper(): string |
140
|
|
|
{ |
141
|
|
|
[$className, $tmpFile] = $this->getFileAndClassName(); |
142
|
|
|
|
143
|
|
|
FileSystem::mkdir(dirname($tmpFile)); |
144
|
|
|
$result = file_put_contents($tmpFile, $this->dumpServiceProviderHelper($className)); |
145
|
|
|
if ($result === false) { |
146
|
|
|
throw IoException::cannotWriteFile($tmpFile); |
147
|
|
|
} |
148
|
|
|
return $tmpFile; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @return string[] Returns an array with 2 items: the class name and the file name. |
153
|
|
|
*/ |
154
|
|
|
private function getFileAndClassName(): array |
155
|
|
|
{ |
156
|
|
|
$refClass = $this->getRefClass(); |
157
|
|
|
$className = $this->getClassName(); |
158
|
|
|
|
159
|
|
|
$fileName = sys_get_temp_dir().'/funky_cache/'. |
160
|
|
|
str_replace(':', '', dirname($refClass->getFileName()).'/'. |
161
|
|
|
str_replace('\\', '__', $className).'.php'); |
162
|
|
|
|
163
|
|
|
return [$className, $fileName]; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
private function getRefClass(): ReflectionClass |
167
|
|
|
{ |
168
|
|
|
if ($this->refClass === null) { |
169
|
|
|
$this->refClass = new ReflectionClass($this); |
170
|
|
|
} |
171
|
|
|
return $this->refClass; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
private function getClassName(): string |
175
|
|
|
{ |
176
|
|
|
$className = get_class($this).'Helper'; |
177
|
|
|
if ($this->getRefClass()->isAnonymous()) { |
|
|
|
|
178
|
|
|
$className = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $className); |
179
|
|
|
} |
180
|
|
|
return $className; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Returns the code of a "service provider helper class" that contains generated factory code. |
185
|
|
|
* |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
|
|
private function dumpServiceProviderHelper(string $className): string |
189
|
|
|
{ |
190
|
|
|
$slashPos = strrpos($className, '\\'); |
191
|
|
|
if ($slashPos !== false) { |
192
|
|
|
$namespace = 'namespace '.substr($className, 0, $slashPos).";\n"; |
193
|
|
|
$shortClassName = substr($className, $slashPos+1); |
194
|
|
|
} else { |
195
|
|
|
$namespace = null; |
196
|
|
|
$shortClassName = $className; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$factoriesArrayCode = []; |
200
|
|
|
$factories = []; |
201
|
|
|
$factoryCount = 0; |
202
|
|
|
|
203
|
|
|
$extensionsArrayCode = []; |
204
|
|
|
$extensions = []; |
205
|
|
|
$extensionCount = 0; |
206
|
|
|
|
207
|
|
|
$factoriesDefinitions = $this->getFactoryDefinitions(); |
208
|
|
|
|
209
|
|
|
foreach ($factoriesDefinitions as $definition) { |
210
|
|
|
if ($definition->isPsrFactory()) { |
211
|
|
|
$factoriesArrayCode[] = ' '.var_export($definition->getName(), true). |
212
|
|
|
' => ['.var_export($definition->getReflectionMethod()->getDeclaringClass()->getName(), true). |
213
|
|
|
', '.var_export($definition->getReflectionMethod()->getName(), true)."],\n"; |
214
|
|
|
} else { |
215
|
|
|
$factoryCount++; |
216
|
|
|
$localFactoryName = 'factory'.$factoryCount; |
217
|
|
|
$factoriesArrayCode[] = ' '.var_export($definition->getName(), true). |
218
|
|
|
' => [self::class, '.var_export($localFactoryName, true)."],\n"; |
219
|
|
|
$factories[] = $definition->buildFactoryCode($localFactoryName); |
220
|
|
|
} |
221
|
|
|
foreach ($definition->getAliases() as $alias) { |
222
|
|
|
$factoriesArrayCode[] = ' '.var_export($alias, true). |
223
|
|
|
' => new Alias('.var_export($definition->getName(), true)."),\n"; |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
$extensionsDefinitions = $this->getExtensionDefinitions(); |
228
|
|
|
|
229
|
|
|
foreach ($extensionsDefinitions as $definition) { |
230
|
|
|
$extensionCount++; |
231
|
|
|
$localExtensionName = 'extension'.$extensionCount; |
232
|
|
|
$extensionsArrayCode[] = ' '.var_export($definition->getName(), true). |
233
|
|
|
' => [self::class, '.var_export($localExtensionName, true)."],\n"; |
234
|
|
|
$extensions[] = $definition->buildExtensionCode($localExtensionName); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
// Now, let's handle tags. |
238
|
|
|
// Let's build an array of tags with a list of services in it. |
239
|
|
|
$tags = []; |
240
|
|
|
foreach ($factoriesDefinitions as $factoryDefinition) { |
241
|
|
|
foreach ($factoryDefinition->getTags() as $tag) { |
242
|
|
|
$tags[$tag->getName()][] = [ |
243
|
|
|
'taggedService' => $factoryDefinition->getName(), |
244
|
|
|
'priority' => $tag->getPriority() |
245
|
|
|
]; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
foreach ($extensionsDefinitions as $extensionDefinition) { |
249
|
|
|
foreach ($extensionDefinition->getTags() as $tag) { |
250
|
|
|
$tags[$tag->getName()][] = [ |
251
|
|
|
'taggedService' => $extensionDefinition->getName(), |
252
|
|
|
'priority' => $tag->getPriority() |
253
|
|
|
]; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
foreach ($tags as $tagName => $taggedServices) { |
258
|
|
|
$tagMethodName = 'tag__'.$tagName; |
259
|
|
|
$tagMethodName = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $tagMethodName); |
260
|
|
|
|
261
|
|
|
$extensionsArrayCode[] = ' '.var_export($tagName, true). |
262
|
|
|
' => [self::class, '.var_export($tagMethodName, true)."],\n"; |
263
|
|
|
$extensions[] = $this->buildTagsCode($tagMethodName, $taggedServices); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
$factoriesArrayStr = implode("\n", $factoriesArrayCode); |
267
|
|
|
$factoriesStr = implode("\n", $factories); |
268
|
|
|
|
269
|
|
|
$extensionsArrayStr = implode("\n", $extensionsArrayCode); |
270
|
|
|
$extensionsStr = implode("\n", $extensions); |
271
|
|
|
|
272
|
|
|
$code = <<<EOF |
273
|
|
|
<?php |
274
|
|
|
$namespace |
275
|
|
|
|
276
|
|
|
use Interop\Container\Factories\Alias; |
277
|
|
|
use Psr\Container\ContainerInterface; |
278
|
|
|
|
279
|
|
|
final class $shortClassName |
280
|
|
|
{ |
281
|
|
|
public static function getFactories(): array |
282
|
|
|
{ |
283
|
|
|
return [ |
284
|
|
|
$factoriesArrayStr |
285
|
|
|
]; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
public static function getExtensions(): array |
289
|
|
|
{ |
290
|
|
|
return [ |
291
|
|
|
$extensionsArrayStr |
292
|
|
|
]; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
$factoriesStr |
296
|
|
|
$extensionsStr |
297
|
|
|
} |
298
|
|
|
EOF; |
299
|
|
|
|
300
|
|
|
return $code; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @param string $tagMethodName |
305
|
|
|
* @param array[] $taggedServices |
306
|
|
|
* @return string |
307
|
|
|
*/ |
308
|
|
|
private function buildTagsCode(string $tagMethodName, array $taggedServices): string |
309
|
|
|
{ |
310
|
|
|
$inserts = []; |
311
|
|
|
|
312
|
|
|
foreach ($taggedServices as $tag) { |
313
|
|
|
['taggedService' => $taggedService, 'priority' => $priority] = $tag; |
314
|
|
|
$inserts[] = sprintf( |
315
|
|
|
' $queue->insert($container->get(%s), %s);', |
316
|
|
|
var_export($taggedService, true), |
317
|
|
|
var_export($priority, true) |
318
|
|
|
); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
|
322
|
|
|
return sprintf( |
323
|
|
|
<<<EOF |
324
|
|
|
public static function %s(ContainerInterface \$container, ?\SplPriorityQueue \$queue): \SplPriorityQueue |
325
|
|
|
{ |
326
|
|
|
\$queue = \$queue ?: new \SplPriorityQueue(); |
327
|
|
|
%s |
328
|
|
|
return \$queue; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
EOF |
332
|
|
|
, |
333
|
|
|
$tagMethodName, |
334
|
|
|
implode("\n", $inserts) |
335
|
|
|
); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.