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'); |
||||
0 ignored issues
–
show
|
|||||
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
The method
getPath() does not exist on ComposerLocator .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
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. ![]() |
|||||
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 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.