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 instanceof Factory) { |
||
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 instanceof Extension) { |
||
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 | $className = $this->getClassName(); |
||
157 | |||
158 | $fileName = \ComposerLocator::getPath('thecodingmachine/funky').'/generated/'. |
||
0 ignored issues
–
show
|
|||
159 | str_replace('\\', '/', $className).'.php'; |
||
160 | |||
161 | return [$className, $fileName]; |
||
162 | } |
||
163 | |||
164 | private function getRefClass(): ReflectionClass |
||
165 | { |
||
166 | if ($this->refClass === null) { |
||
167 | $this->refClass = new ReflectionClass($this); |
||
168 | } |
||
169 | return $this->refClass; |
||
170 | } |
||
171 | |||
172 | private function getClassName(): string |
||
173 | { |
||
174 | $className = 'FunkyGenerated\\'.\get_class($this).'Helper'; |
||
175 | if ($this->getRefClass()->isAnonymous()) { |
||
176 | $className = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $className); |
||
177 | } |
||
178 | return $className; |
||
179 | } |
||
180 | |||
181 | /** |
||
182 | * Returns the code of a "service provider helper class" that contains generated factory code. |
||
183 | * |
||
184 | * @return string |
||
185 | */ |
||
186 | private function dumpServiceProviderHelper(string $className): string |
||
187 | { |
||
188 | $slashPos = strrpos($className, '\\'); |
||
189 | if ($slashPos !== false) { |
||
190 | $namespace = 'namespace '.substr($className, 0, $slashPos).";\n"; |
||
191 | $shortClassName = substr($className, $slashPos+1); |
||
192 | } else { |
||
193 | $namespace = null; |
||
194 | $shortClassName = $className; |
||
195 | } |
||
196 | |||
197 | $factoriesArrayCode = []; |
||
198 | $factories = []; |
||
199 | $factoryCount = 0; |
||
200 | |||
201 | $extensionsArrayCode = []; |
||
202 | $extensions = []; |
||
203 | $extensionCount = 0; |
||
204 | |||
205 | $factoriesDefinitions = $this->getFactoryDefinitions(); |
||
206 | |||
207 | foreach ($factoriesDefinitions as $definition) { |
||
208 | if ($definition->isPsrFactory()) { |
||
209 | $factoriesArrayCode[] = ' '.var_export($definition->getName(), true). |
||
210 | ' => ['.var_export($definition->getReflectionMethod()->getDeclaringClass()->getName(), true). |
||
211 | ', '.var_export($definition->getReflectionMethod()->getName(), true)."],\n"; |
||
212 | } else { |
||
213 | $factoryCount++; |
||
214 | $localFactoryName = 'factory'.$factoryCount; |
||
215 | $factoriesArrayCode[] = ' '.var_export($definition->getName(), true). |
||
216 | ' => [self::class, '.var_export($localFactoryName, true)."],\n"; |
||
217 | $factories[] = $definition->buildFactoryCode($localFactoryName); |
||
218 | } |
||
219 | foreach ($definition->getAliases() as $alias) { |
||
220 | $factoriesArrayCode[] = ' '.var_export($alias, true). |
||
221 | ' => new Alias('.var_export($definition->getName(), true)."),\n"; |
||
222 | } |
||
223 | } |
||
224 | |||
225 | $extensionsDefinitions = $this->getExtensionDefinitions(); |
||
226 | |||
227 | foreach ($extensionsDefinitions as $definition) { |
||
228 | $extensionCount++; |
||
229 | $localExtensionName = 'extension'.$extensionCount; |
||
230 | $extensionsArrayCode[] = ' '.var_export($definition->getName(), true). |
||
231 | ' => [self::class, '.var_export($localExtensionName, true)."],\n"; |
||
232 | $extensions[] = $definition->buildExtensionCode($localExtensionName); |
||
233 | } |
||
234 | |||
235 | // Now, let's handle tags. |
||
236 | // Let's build an array of tags with a list of services in it. |
||
237 | $tags = []; |
||
238 | foreach ($factoriesDefinitions as $factoryDefinition) { |
||
239 | foreach ($factoryDefinition->getTags() as $tag) { |
||
240 | $tags[$tag->getName()][] = [ |
||
241 | 'taggedService' => $factoryDefinition->getName(), |
||
242 | 'priority' => $tag->getPriority() |
||
243 | ]; |
||
244 | } |
||
245 | } |
||
246 | foreach ($extensionsDefinitions as $extensionDefinition) { |
||
247 | foreach ($extensionDefinition->getTags() as $tag) { |
||
248 | $tags[$tag->getName()][] = [ |
||
249 | 'taggedService' => $extensionDefinition->getName(), |
||
250 | 'priority' => $tag->getPriority() |
||
251 | ]; |
||
252 | } |
||
253 | } |
||
254 | |||
255 | foreach ($tags as $tagName => $taggedServices) { |
||
256 | $tagMethodName = 'tag__'.$tagName; |
||
257 | $tagMethodName = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $tagMethodName); |
||
258 | |||
259 | $extensionsArrayCode[] = ' '.var_export($tagName, true). |
||
260 | ' => [self::class, '.var_export($tagMethodName, true)."],\n"; |
||
261 | $extensions[] = $this->buildTagsCode($tagMethodName, $taggedServices); |
||
262 | } |
||
263 | |||
264 | $factoriesArrayStr = implode("\n", $factoriesArrayCode); |
||
265 | $factoriesStr = implode("\n", $factories); |
||
266 | |||
267 | $extensionsArrayStr = implode("\n", $extensionsArrayCode); |
||
268 | $extensionsStr = implode("\n", $extensions); |
||
269 | |||
270 | $code = <<<EOF |
||
271 | <?php |
||
272 | $namespace |
||
273 | |||
274 | use Interop\Container\Factories\Alias; |
||
275 | use Psr\Container\ContainerInterface; |
||
276 | |||
277 | final class $shortClassName |
||
278 | { |
||
279 | public static function getFactories(): array |
||
280 | { |
||
281 | return [ |
||
282 | $factoriesArrayStr |
||
283 | ]; |
||
284 | } |
||
285 | |||
286 | public static function getExtensions(): array |
||
287 | { |
||
288 | return [ |
||
289 | $extensionsArrayStr |
||
290 | ]; |
||
291 | } |
||
292 | |||
293 | $factoriesStr |
||
294 | $extensionsStr |
||
295 | } |
||
296 | EOF; |
||
297 | |||
298 | return $code; |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param string $tagMethodName |
||
303 | * @param array[] $taggedServices |
||
304 | * @return string |
||
305 | */ |
||
306 | private function buildTagsCode(string $tagMethodName, array $taggedServices): string |
||
307 | { |
||
308 | $inserts = []; |
||
309 | |||
310 | foreach ($taggedServices as $tag) { |
||
311 | ['taggedService' => $taggedService, 'priority' => $priority] = $tag; |
||
312 | $inserts[] = sprintf( |
||
313 | ' $queue->insert($container->get(%s), %s);', |
||
314 | var_export($taggedService, true), |
||
315 | var_export($priority, true) |
||
316 | ); |
||
317 | } |
||
318 | |||
319 | |||
320 | return sprintf( |
||
321 | <<<EOF |
||
322 | public static function %s(ContainerInterface \$container, ?\SplPriorityQueue \$queue = null): \SplPriorityQueue |
||
323 | { |
||
324 | \$queue = \$queue ?: new \SplPriorityQueue(); |
||
325 | %s |
||
326 | return \$queue; |
||
327 | } |
||
328 | |||
329 | EOF |
||
330 | , |
||
331 | $tagMethodName, |
||
332 | implode("\n", $inserts) |
||
333 | ); |
||
334 | } |
||
335 | } |
||
336 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.