Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 316 |
|
0.00% |
0 / 31 |
CRAP | |
0.00% |
0 / 1 |
GeneratorTools | |
0.00% |
0 / 316 |
|
0.00% |
0 / 31 |
10920 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPluginManagerForNamespace | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getVuFindExtendedModules | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getPluginManagerFromExplodedClassName | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
getShortNameFromExplodedClassName | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getExpectedInterfaceFromPluginManager | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getConfigPathForClass | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getPluginManagerForClassParts | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
createPlugin | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
generateFactory | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
extendClass | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
110 | |||
getAllFactoriesFromContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getFactoryFromContainer | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getAllDelegatorsFromContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDelegatorsFromContainer | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPluginManagerContainingClass | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
extendService | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 | |||
cloneFactory | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
132 | |||
updateFactory | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
generateLocalClassName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
createClassInModule | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
writeClass | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
createSubclassInModule | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
createTree | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
backUpFile | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getModuleConfigPath | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
writeModuleConfig | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
applySettingToConfig | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
writeNewConfigs | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
writeNewConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
retrieveConfig | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * Generator tools. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2018. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Generator |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFindConsole\Generator; |
31 | |
32 | use Laminas\Code\Generator\ClassGenerator; |
33 | use Laminas\Code\Generator\FileGenerator; |
34 | use Laminas\Code\Generator\MethodGenerator; |
35 | use Laminas\Code\Reflection\ClassReflection; |
36 | use Psr\Container\ContainerInterface; |
37 | |
38 | use function count; |
39 | use function in_array; |
40 | use function is_array; |
41 | use function is_callable; |
42 | use function is_string; |
43 | use function strlen; |
44 | |
45 | /** |
46 | * Generator tools. |
47 | * |
48 | * @category VuFind |
49 | * @package Generator |
50 | * @author Demian Katz <demian.katz@villanova.edu> |
51 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
52 | * @link https://vufind.org/wiki/development Wiki |
53 | */ |
54 | class GeneratorTools |
55 | { |
56 | use \VuFindConsole\ConsoleOutputTrait; |
57 | |
58 | /** |
59 | * Laminas configuration |
60 | * |
61 | * @var array |
62 | */ |
63 | protected $config; |
64 | |
65 | /** |
66 | * Constructor. |
67 | * |
68 | * @param array $config Laminas configuration |
69 | */ |
70 | public function __construct(array $config) |
71 | { |
72 | $this->config = $config; |
73 | } |
74 | |
75 | /** |
76 | * Determine a plugin manager name within the specified namespace. |
77 | * |
78 | * @param array $classParts Exploded class name array |
79 | * @param string $namespace Namespace to try for plugin manager |
80 | * |
81 | * @return string |
82 | */ |
83 | protected function getPluginManagerForNamespace($classParts, $namespace) |
84 | { |
85 | $classParts[0] = $namespace; |
86 | $classParts[count($classParts) - 1] = 'PluginManager'; |
87 | return implode('\\', $classParts); |
88 | } |
89 | |
90 | /** |
91 | * Get a list of VuFind modules (only those with names beginning with VuFind, |
92 | * and not including the core VuFind module itself). |
93 | * |
94 | * @return array |
95 | */ |
96 | protected function getVuFindExtendedModules() |
97 | { |
98 | $moduleDir = __DIR__ . '/../../../../'; |
99 | $handle = opendir($moduleDir); |
100 | $results = []; |
101 | while ($line = readdir($handle)) { |
102 | if (str_starts_with($line, 'VuFind') && strlen($line) > 6) { |
103 | $results[] = $line; |
104 | } |
105 | } |
106 | closedir($handle); |
107 | return $results; |
108 | } |
109 | |
110 | /** |
111 | * Given a class name exploded into an array, figure out the appropriate plugin |
112 | * manager to use. |
113 | * |
114 | * @param array $classParts Exploded class name array |
115 | * |
116 | * @return string |
117 | */ |
118 | protected function getPluginManagerFromExplodedClassName($classParts) |
119 | { |
120 | $pmClass = $this->getPluginManagerForNamespace($classParts, 'VuFind'); |
121 | // Special cases: no such service; use framework core services instead: |
122 | if ($pmClass === 'VuFind\Controller\PluginManager') { |
123 | return 'ControllerManager'; |
124 | } |
125 | if ($pmClass === 'VuFind\Controller\Plugin\PluginManager') { |
126 | return \Laminas\Mvc\Controller\PluginManager::class; |
127 | } |
128 | // Special case: no such service; check other modules: |
129 | if (!class_exists($pmClass)) { |
130 | foreach ($this->getVuFindExtendedModules() as $module) { |
131 | $pmClass = $this->getPluginManagerForNamespace($classParts, $module); |
132 | if (class_exists($pmClass)) { |
133 | break; |
134 | } |
135 | } |
136 | } |
137 | return $pmClass; |
138 | } |
139 | |
140 | /** |
141 | * Given a class name exploded into an array, figure out the appropriate short |
142 | * name to use as an alias in the service manager configuration. |
143 | * |
144 | * @param array $classParts Exploded class name array |
145 | * |
146 | * @return string |
147 | */ |
148 | protected function getShortNameFromExplodedClassName($classParts) |
149 | { |
150 | $shortName = array_pop($classParts); |
151 | // Special case: controllers use shortened aliases |
152 | if (($classParts[1] ?? '') === 'Controller') { |
153 | return preg_replace('/Controller$/', '', $shortName); |
154 | } |
155 | return strtolower($shortName); |
156 | } |
157 | |
158 | /** |
159 | * Given a plugin manager object, return the interface plugins of that type must |
160 | * implement. |
161 | * |
162 | * @param ContainerInterface $pm Plugin manager |
163 | * |
164 | * @return string |
165 | */ |
166 | protected function getExpectedInterfaceFromPluginManager($pm) |
167 | { |
168 | // Special case: controllers |
169 | if ($pm instanceof \Laminas\Mvc\Controller\ControllerManager) { |
170 | return \VuFind\Controller\AbstractBase::class; |
171 | } |
172 | |
173 | // Special case: controller plugins: |
174 | if ($pm instanceof \Laminas\Mvc\Controller\PluginManager) { |
175 | return \Laminas\Mvc\Controller\Plugin\AbstractPlugin::class; |
176 | } |
177 | |
178 | // Default case: look it up: |
179 | if (!method_exists($pm, 'getExpectedInterface')) { |
180 | return null; |
181 | } |
182 | |
183 | // Force getExpectedInterface() to be public so we can read it: |
184 | $reflectionMethod = new \ReflectionMethod($pm, 'getExpectedInterface'); |
185 | $reflectionMethod->setAccessible(true); |
186 | return $reflectionMethod->invoke($pm); |
187 | } |
188 | |
189 | /** |
190 | * Given a plugin manager class name, return the configuration path for that |
191 | * plugin manager. |
192 | * |
193 | * @param string $class Class name |
194 | * |
195 | * @return array |
196 | */ |
197 | protected function getConfigPathForClass($class) |
198 | { |
199 | // Special case: controller |
200 | if ($class === \Laminas\Mvc\Controller\ControllerManager::class) { |
201 | return ['controllers']; |
202 | } elseif ($class == \Laminas\Mvc\Controller\PluginManager::class) { |
203 | return ['controller_plugins']; |
204 | } elseif ($class == \Laminas\ServiceManager\ServiceManager::class) { |
205 | return ['service_manager']; |
206 | } |
207 | // Default case: VuFind internal plugin manager |
208 | $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory(); |
209 | $pmKey = $apmFactory->getConfigKey($class); |
210 | return ['vufind', 'plugin_managers', $pmKey]; |
211 | } |
212 | |
213 | /** |
214 | * Given appropriate inputs, figure out which plugin manager or service manager |
215 | * to use during plugin generation. |
216 | * |
217 | * @param ContainerInterface $container Service manager |
218 | * @param array $classParts Exploded class name array |
219 | * @param bool $topLevelService Set to true to build a service |
220 | * in the top-level container rather than a plugin in a subsidiary plugin manager |
221 | * |
222 | * @return ContainerInterface |
223 | */ |
224 | protected function getPluginManagerForClassParts( |
225 | $container, |
226 | $classParts, |
227 | $topLevelService |
228 | ) { |
229 | // Special case -- short-circuit for top-level service: |
230 | if ($topLevelService) { |
231 | return $container; |
232 | } |
233 | $pmClass = $this->getPluginManagerFromExplodedClassName($classParts); |
234 | if (!$container->has($pmClass)) { |
235 | throw new \Exception( |
236 | 'Cannot find expected plugin manager: ' . $pmClass . "\n" |
237 | . 'You can use the --top-level option if you wish to create' |
238 | . ' a top-level service.' |
239 | ); |
240 | } |
241 | return $container->get($pmClass); |
242 | } |
243 | |
244 | /** |
245 | * Create a plugin class. |
246 | * |
247 | * @param ContainerInterface $container Service manager |
248 | * @param string $class Class name to create |
249 | * @param string $factory Existing factory to use (null to |
250 | * generate a new one) |
251 | * @param bool $topLevelService Set to true to build a service |
252 | * in the top-level container rather than a plugin in a subsidiary plugin manager |
253 | * |
254 | * @return bool |
255 | * @throws \Exception |
256 | */ |
257 | public function createPlugin( |
258 | ContainerInterface $container, |
259 | $class, |
260 | $factory = null, |
261 | $topLevelService = false |
262 | ) { |
263 | // Derive some key bits of information from the new class name: |
264 | $classParts = explode('\\', $class); |
265 | $module = $classParts[0]; |
266 | $shortName = $this->getShortNameFromExplodedClassName($classParts); |
267 | |
268 | // Set a flag for whether to generate a factory, and create class name |
269 | // if necessary. If existing factory specified, ensure it really exists. |
270 | if ($generateFactory = empty($factory)) { |
271 | $factory = $class . 'Factory'; |
272 | } elseif (!class_exists($factory)) { |
273 | throw new \Exception("Undefined factory: $factory"); |
274 | } |
275 | |
276 | // Figure out further information based on the plugin manager: |
277 | $pm = $this->getPluginManagerForClassParts( |
278 | $container, |
279 | $classParts, |
280 | $topLevelService |
281 | ); |
282 | $interface = $this->getExpectedInterfaceFromPluginManager($pm); |
283 | |
284 | // Figure out whether the plugin requirement is an interface or a |
285 | // parent class so we can create the right thing.... |
286 | if (interface_exists($interface)) { |
287 | $parent = null; |
288 | $interfaces = [$interface]; |
289 | } else { |
290 | $parent = $interface; |
291 | $interfaces = []; |
292 | } |
293 | $configPath = $this->getConfigPathForClass($pm::class); |
294 | |
295 | // Generate the classes and configuration: |
296 | $this->createClassInModule($class, $module, $parent, $interfaces); |
297 | if ($generateFactory) { |
298 | $this->generateFactory($factory, $module); |
299 | } |
300 | $factoryPath = array_merge($configPath, ['factories', $class]); |
301 | $aliasPath = array_merge($configPath, ['aliases', $shortName]); |
302 | $newConfigs = [ |
303 | ['path' => $factoryPath, 'setting' => $factory], |
304 | ['path' => $aliasPath, 'setting' => $class], |
305 | ]; |
306 | // Add extra lowercase alias if necessary: |
307 | if (strtolower($shortName) != $shortName) { |
308 | $lowerAliasPath = array_merge( |
309 | $configPath, |
310 | ['aliases', strtolower($shortName)] |
311 | ); |
312 | $newConfigs[] = ['path' => $lowerAliasPath, 'setting' => $class]; |
313 | } |
314 | $this->writeNewConfigs($newConfigs, $module, false); |
315 | |
316 | return true; |
317 | } |
318 | |
319 | /** |
320 | * Generate a factory class. |
321 | * |
322 | * @param string $factory Name of factory to generate |
323 | * @param string $module Name of module to generate factory within |
324 | * |
325 | * @return void |
326 | */ |
327 | protected function generateFactory($factory, $module) |
328 | { |
329 | $this->createClassInModule( |
330 | $factory, |
331 | $module, |
332 | null, |
333 | ['Laminas\ServiceManager\Factory\FactoryInterface'], |
334 | function ($generator) { |
335 | $method = MethodGenerator::fromArray( |
336 | [ |
337 | 'name' => '__invoke', |
338 | 'body' => 'return new $requestedName();', |
339 | ] |
340 | ); |
341 | $param1 = [ |
342 | 'name' => 'container', |
343 | 'type' => 'Psr\Container\ContainerInterface', |
344 | ]; |
345 | $param2 = [ |
346 | 'name' => 'requestedName', |
347 | ]; |
348 | $param3 = [ |
349 | 'name' => 'options', |
350 | 'type' => 'array', |
351 | 'defaultValue' => null, |
352 | ]; |
353 | $method->setParameters([$param1, $param2, $param3]); |
354 | // Copy doc block from this class' factory: |
355 | $reflection = new \Laminas\Code\Reflection\MethodReflection( |
356 | GeneratorToolsFactory::class, |
357 | '__invoke' |
358 | ); |
359 | $example = MethodGenerator::fromReflection($reflection); |
360 | $method->setDocBlock($example->getDocBlock()); |
361 | $generator->addMethods([$method]); |
362 | } |
363 | ); |
364 | } |
365 | |
366 | /** |
367 | * Extend a class defined somewhere in the service manager or its child |
368 | * plugin managers. |
369 | * |
370 | * @param ContainerInterface $container Service manager |
371 | * @param string $class Class name to extend |
372 | * @param string $target Target module in which to create new |
373 | * service |
374 | * @param bool $extendFactory Should we extend the factory? |
375 | * |
376 | * @return bool |
377 | * @throws \Exception |
378 | */ |
379 | public function extendClass( |
380 | ContainerInterface $container, |
381 | $class, |
382 | $target, |
383 | $extendFactory = false |
384 | ) { |
385 | // Set things up differently depending on whether this is a top-level |
386 | // service or a class in a plugin manager. |
387 | $cm = $container->get('ControllerManager'); |
388 | $cpm = $container->get('ControllerPluginManager'); |
389 | $configPath = []; |
390 | $delegators = []; |
391 | if ($container->has($class)) { |
392 | $factory = $this->getFactoryFromContainer($container, $class); |
393 | $configPath = ['service_manager']; |
394 | } elseif ($factory = $this->getFactoryFromContainer($cm, $class)) { |
395 | $configPath = ['controllers']; |
396 | } elseif ($factory = $this->getFactoryFromContainer($cpm, $class)) { |
397 | $configPath = ['controller_plugins']; |
398 | } elseif ($pm = $this->getPluginManagerContainingClass($container, $class)) { |
399 | $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory(); |
400 | $pmKey = $apmFactory->getConfigKey($pm::class); |
401 | $factory = $this->getFactoryFromContainer($pm, $class); |
402 | $configPath = ['vufind', 'plugin_managers', $pmKey]; |
403 | $delegators = $this->getDelegatorsFromContainer($pm, $class); |
404 | } |
405 | |
406 | // No factory found? Throw an error! |
407 | if (empty($factory)) { |
408 | throw new \Exception('Could not find factory for ' . $class); |
409 | } |
410 | |
411 | // Create the custom subclass. |
412 | $newClass = $this->createSubclassInModule($class, $target); |
413 | |
414 | // Create the custom factory only if requested. |
415 | $newFactory = $extendFactory |
416 | ? $this->cloneFactory($factory, $target) : $factory; |
417 | |
418 | // Finalize the local module configuration -- create a factory for the |
419 | // new class, and set up the new class as an alias for the old class. |
420 | $factoryPath = array_merge($configPath, ['factories', $newClass]); |
421 | $aliasPath = array_merge($configPath, ['aliases', $class]); |
422 | $newConfigs = [ |
423 | ['path' => $factoryPath, 'setting' => $newFactory], |
424 | ['path' => $aliasPath, 'setting' => $newClass], |
425 | ]; |
426 | |
427 | // Clone/configure delegator factories as needed. |
428 | if (!empty($delegators)) { |
429 | $newDelegators = []; |
430 | foreach ($delegators as $delegator) { |
431 | $newDelegators[] = $extendFactory |
432 | ? $this->cloneFactory($delegator, $target) : $delegator; |
433 | } |
434 | $delegatorPath = array_merge($configPath, ['delegators', $newClass]); |
435 | $newConfigs[] = ['path' => $delegatorPath, 'setting' => $newDelegators]; |
436 | } |
437 | $this->writeNewConfigs($newConfigs, $target, false); |
438 | |
439 | return true; |
440 | } |
441 | |
442 | /** |
443 | * Get a list of factories in the provided container. |
444 | * |
445 | * @param ContainerInterface $container Container to inspect |
446 | * |
447 | * @return array |
448 | */ |
449 | protected function getAllFactoriesFromContainer(ContainerInterface $container) |
450 | { |
451 | // There is no "getFactories" method, so we need to use reflection: |
452 | $reflectionProperty = new \ReflectionProperty($container, 'factories'); |
453 | $reflectionProperty->setAccessible(true); |
454 | return $reflectionProperty->getValue($container); |
455 | } |
456 | |
457 | /** |
458 | * Get a factory from the provided container (or null if undefined). |
459 | * |
460 | * @param ContainerInterface $container Container to inspect |
461 | * @param string $class Class whose factory we want |
462 | * |
463 | * @return string |
464 | */ |
465 | protected function getFactoryFromContainer(ContainerInterface $container, $class) |
466 | { |
467 | $factories = $this->getAllFactoriesFromContainer($container); |
468 | return $factories[$class] ?? null; |
469 | } |
470 | |
471 | /** |
472 | * Get a list of delegators in the provided container. |
473 | * |
474 | * @param ContainerInterface $container Container to inspect |
475 | * |
476 | * @return array |
477 | */ |
478 | protected function getAllDelegatorsFromContainer(ContainerInterface $container) |
479 | { |
480 | // There is no "getDelegators" method, so we need to use reflection: |
481 | $reflectionProperty = new \ReflectionProperty($container, 'delegators'); |
482 | $reflectionProperty->setAccessible(true); |
483 | return $reflectionProperty->getValue($container); |
484 | } |
485 | |
486 | /** |
487 | * Get delegators from the provided container (or empty array if undefined). |
488 | * |
489 | * @param ContainerInterface $container Container to inspect |
490 | * @param string $class Class whose delegators we want |
491 | * |
492 | * @return array |
493 | */ |
494 | protected function getDelegatorsFromContainer( |
495 | ContainerInterface $container, |
496 | $class |
497 | ) { |
498 | $delegators = $this->getAllDelegatorsFromContainer($container); |
499 | return $delegators[$class] ?? []; |
500 | } |
501 | |
502 | /** |
503 | * Search all plugin managers for one containing the requested class (or return |
504 | * null if none found). |
505 | * |
506 | * @param ContainerInterface $container Service manager |
507 | * @param string $class Class to search for |
508 | * |
509 | * @return ContainerInterface |
510 | */ |
511 | protected function getPluginManagerContainingClass( |
512 | ContainerInterface $container, |
513 | $class |
514 | ) { |
515 | $factories = $this->getAllFactoriesFromContainer($container); |
516 | foreach (array_keys($factories) as $service) { |
517 | if (str_ends_with($service, 'PluginManager')) { |
518 | $pm = $container->get($service); |
519 | if (null !== $this->getFactoryFromContainer($pm, $class)) { |
520 | return $pm; |
521 | } |
522 | } |
523 | } |
524 | return null; |
525 | } |
526 | |
527 | /** |
528 | * Extend a service defined in module.config.php. |
529 | * |
530 | * @param string $source Configuration path to use as source |
531 | * @param string $target Target module in which to create new service |
532 | * |
533 | * @return bool |
534 | * @throws \Exception |
535 | */ |
536 | public function extendService($source, $target) |
537 | { |
538 | $parts = explode('/', $source); |
539 | $partCount = count($parts); |
540 | if ($partCount < 3) { |
541 | throw new \Exception('Config path too short.'); |
542 | } |
543 | $sourceType = $parts[$partCount - 2]; |
544 | |
545 | $supportedTypes = ['factories', 'invokables']; |
546 | if (!in_array($sourceType, $supportedTypes)) { |
547 | throw new \Exception( |
548 | 'Unsupported service type; supported values: ' |
549 | . implode(', ', $supportedTypes) |
550 | ); |
551 | } |
552 | |
553 | $config = $this->retrieveConfig($parts); |
554 | if (!$config) { |
555 | throw new \Exception("{$source} not found in configuration."); |
556 | } |
557 | |
558 | switch ($sourceType) { |
559 | case 'factories': |
560 | $this->createSubclassInModule($parts[$partCount - 1], $target); |
561 | $newConfig = $this->cloneFactory($config, $target); |
562 | break; |
563 | case 'invokables': |
564 | $newConfig = $this->createSubclassInModule($config, $target); |
565 | break; |
566 | default: |
567 | throw new \Exception('Reached unreachable code!'); |
568 | } |
569 | $this->writeNewConfig($parts, $newConfig, $target); |
570 | return true; |
571 | } |
572 | |
573 | /** |
574 | * Create a new subclass and factory to override a factory-generated |
575 | * service. |
576 | * |
577 | * @param mixed $factory Factory configuration for class to extend |
578 | * @param string $module Module in which to create the new factory |
579 | * |
580 | * @return string |
581 | * @throws \Exception |
582 | */ |
583 | protected function cloneFactory($factory, $module) |
584 | { |
585 | // If the factory is a stand-alone class, it's simple to clone: |
586 | if (class_exists($factory)) { |
587 | return $this->createSubclassInModule($factory, $module); |
588 | } |
589 | |
590 | // Make sure we can figure out how to handle the factory; it should |
591 | // either be a [controller, method] array or a "controller::method" |
592 | // string; anything else will cause a problem. |
593 | $parts = is_string($factory) ? explode('::', $factory) : $factory; |
594 | if ( |
595 | !is_array($parts) || count($parts) != 2 || !class_exists($parts[0]) |
596 | || !is_callable($parts) |
597 | ) { |
598 | throw new \Exception('Unexpected factory configuration format.'); |
599 | } |
600 | [$factoryClass, $factoryMethod] = $parts; |
601 | $newFactoryClass = $this->generateLocalClassName($factoryClass, $module); |
602 | if (!class_exists($newFactoryClass)) { |
603 | $this->createSubclassInModule($factoryClass, $module); |
604 | $skipBackup = true; |
605 | } else { |
606 | $skipBackup = false; |
607 | } |
608 | |
609 | $oldReflection = new ClassReflection($factoryClass); |
610 | $newReflection = new ClassReflection($newFactoryClass); |
611 | |
612 | try { |
613 | $newMethod = $newReflection->getMethod($factoryMethod); |
614 | if ($newMethod->getDeclaringClass()->getName() == $newFactoryClass) { |
615 | throw new \Exception( |
616 | "$newFactoryClass::$factoryMethod already exists." |
617 | ); |
618 | } |
619 | |
620 | $generator = ClassGenerator::fromReflection($newReflection); |
621 | $method = MethodGenerator::fromReflection( |
622 | $oldReflection->getMethod($factoryMethod) |
623 | ); |
624 | $this->updateFactory( |
625 | $method, |
626 | $oldReflection->getNamespaceName(), |
627 | $module |
628 | ); |
629 | $generator->addMethodFromGenerator($method); |
630 | $this->writeClass($generator, $module, true, $skipBackup); |
631 | } catch (\ReflectionException $e) { |
632 | // If a parent factory has a __callStatic method, the method we are |
633 | // trying to rewrite may not exist. In that case, we can just inherit |
634 | // __callStatic and ignore the error. Any other exception should be |
635 | // treated as a fatal error. |
636 | if (method_exists($factoryClass, '__callStatic')) { |
637 | $this->writeln('Error: ' . $e->getMessage()); |
638 | $this->writeln( |
639 | '__callStatic in parent factory; skipping method generation.' |
640 | ); |
641 | } else { |
642 | throw $e; |
643 | } |
644 | } |
645 | |
646 | return $newFactoryClass . '::' . $factoryMethod; |
647 | } |
648 | |
649 | /** |
650 | * Given a factory method, extend the class being constructed and create |
651 | * a new factory for the subclass. |
652 | * |
653 | * @param MethodGenerator $method Method to modify |
654 | * @param string $ns Namespace of old factory |
655 | * @param string $module Module in which to make changes |
656 | * |
657 | * @return void |
658 | * @throws \Exception |
659 | */ |
660 | protected function updateFactory( |
661 | MethodGenerator $method, |
662 | $ns, |
663 | $module |
664 | ) { |
665 | $body = $method->getBody(); |
666 | $regex = '/new\s+([\w\\\\]*)\s*\(/m'; |
667 | preg_match_all($regex, $body, $matches); |
668 | $classNames = $matches[1]; |
669 | $count = count($classNames); |
670 | if ($count != 1) { |
671 | throw new \Exception("Found $count class names; expected 1."); |
672 | } |
673 | $className = $classNames[0]; |
674 | // Figure out fully qualified name for purposes of createSubclassInModule(): |
675 | $fqClassName = (!str_starts_with($className, '\\')) |
676 | ? "$ns\\$className" : $className; |
677 | $newClass = $this->generateLocalClassName($fqClassName, $module); |
678 | $body = preg_replace( |
679 | '/new\s+' . addslashes($className) . '\s*\(/m', |
680 | 'new \\' . $newClass . '(', |
681 | $body |
682 | ); |
683 | $method->setBody($body); |
684 | } |
685 | |
686 | /** |
687 | * Determine the name of a local replacement class within the specified |
688 | * module. |
689 | * |
690 | * @param string $class Name of class to extend/replace |
691 | * @param string $module Module in which to create the new class |
692 | * |
693 | * @return string |
694 | * @throws \Exception |
695 | */ |
696 | protected function generateLocalClassName($class, $module) |
697 | { |
698 | // Determine the name of the new class by exploding the old class and |
699 | // replacing the namespace: |
700 | $parts = explode('\\', trim($class, '\\')); |
701 | if (count($parts) < 2) { |
702 | throw new \Exception('Expected a namespaced class; found ' . $class); |
703 | } |
704 | $parts[0] = $module; |
705 | return implode('\\', $parts); |
706 | } |
707 | |
708 | /** |
709 | * Extend a specified class within a specified module. Return the name of |
710 | * the new subclass. |
711 | * |
712 | * @param string $class Name of class to create |
713 | * @param string $module Module in which to create the new class |
714 | * @param string $parent Parent class (null for no parent) |
715 | * @param string[] $interfaces Interfaces for class to implement |
716 | * @param callable $callback Callback to set up class generator |
717 | * |
718 | * @return void |
719 | * @throws \Exception |
720 | */ |
721 | protected function createClassInModule( |
722 | $class, |
723 | $module, |
724 | $parent = null, |
725 | array $interfaces = [], |
726 | $callback = null |
727 | ) { |
728 | $generator = new ClassGenerator($class, null, null, $parent, $interfaces); |
729 | if (is_callable($callback)) { |
730 | $callback($generator); |
731 | } |
732 | $this->writeClass($generator, $module); |
733 | } |
734 | |
735 | /** |
736 | * Write a class to disk. |
737 | * |
738 | * @param ClassGenerator $classGenerator Representation of class to write |
739 | * @param string $module Module in which to write class |
740 | * @param bool $allowOverwrite Allow overwrite of existing file? |
741 | * @param bool $skipBackup Should we skip backing up the file? |
742 | * |
743 | * @return void |
744 | * @throws \Exception |
745 | */ |
746 | protected function writeClass( |
747 | ClassGenerator $classGenerator, |
748 | $module, |
749 | $allowOverwrite = false, |
750 | $skipBackup = false |
751 | ) { |
752 | // Use the class name parts from the previous step to determine a path |
753 | // and filename, then create the new path. |
754 | $parts = explode('\\', $classGenerator->getNamespaceName()); |
755 | array_unshift($parts, 'module', $module, 'src'); |
756 | $this->createTree($parts); |
757 | |
758 | // Generate the new class: |
759 | $generator = FileGenerator::fromArray(['classes' => [$classGenerator]]); |
760 | $filename = $classGenerator->getName() . '.php'; |
761 | $fullPath = APPLICATION_PATH . '/' . implode('/', $parts) . '/' . $filename; |
762 | if (file_exists($fullPath)) { |
763 | if ($allowOverwrite) { |
764 | if (!$skipBackup) { |
765 | $this->backUpFile($fullPath); |
766 | } |
767 | } else { |
768 | throw new \Exception("$fullPath already exists."); |
769 | } |
770 | } |
771 | // TODO: this is a workaround for an apparent bug in Laminas\Code which |
772 | // omits the leading backslash on "extends" statements when rewriting |
773 | // existing classes. Can we remove this after a future Laminas\Code upgrade? |
774 | $code = str_replace( |
775 | 'extends VuFind\\', |
776 | 'extends \\VuFind\\', |
777 | $generator->generate() |
778 | ); |
779 | if (!file_put_contents($fullPath, $code)) { |
780 | throw new \Exception("Problem writing to $fullPath."); |
781 | } |
782 | $this->writeln("Saved file: $fullPath"); |
783 | } |
784 | |
785 | /** |
786 | * Extend a specified class within a specified module. Return the name of |
787 | * the new subclass. |
788 | * |
789 | * @param string $class Name of class to extend |
790 | * @param string $module Module in which to create the new class |
791 | * |
792 | * @return string |
793 | * @throws \Exception |
794 | */ |
795 | protected function createSubclassInModule($class, $module) |
796 | { |
797 | // Normalize leading backslashes; in some contexts we will |
798 | // have them and in others we may not. |
799 | $class = trim($class, '\\'); |
800 | $newClass = $this->generateLocalClassName($class, $module); |
801 | $this->createClassInModule($newClass, $module, "\\$class"); |
802 | return $newClass; |
803 | } |
804 | |
805 | /** |
806 | * Create a directory tree. |
807 | * |
808 | * @param array $path Array of subdirectories to create relative to |
809 | * APPLICATION_PATH |
810 | * |
811 | * @return void |
812 | * @throws \Exception |
813 | */ |
814 | protected function createTree($path) |
815 | { |
816 | $fullPath = APPLICATION_PATH; |
817 | foreach ($path as $part) { |
818 | $fullPath .= '/' . $part; |
819 | if (!file_exists($fullPath)) { |
820 | if (!mkdir($fullPath)) { |
821 | throw new \Exception("Problem creating $fullPath"); |
822 | } |
823 | } |
824 | if (!is_dir($fullPath)) { |
825 | throw new \Exception("$fullPath is not a directory!"); |
826 | } |
827 | } |
828 | } |
829 | |
830 | /** |
831 | * Create a backup of a file. |
832 | * |
833 | * @param string $filename File to back up |
834 | * |
835 | * @return void |
836 | * @throws \Exception |
837 | */ |
838 | public function backUpFile($filename) |
839 | { |
840 | $backup = $filename . '.' . time() . '.bak'; |
841 | if (!copy($filename, $backup)) { |
842 | throw new \Exception("Problem generating backup file: $backup"); |
843 | } |
844 | $this->writeln("Created backup: $backup"); |
845 | } |
846 | |
847 | /** |
848 | * Get the path to the module configuration; throw an exception if it is |
849 | * missing. |
850 | * |
851 | * @param string $module Module name |
852 | * |
853 | * @return string |
854 | * @throws \Exception |
855 | */ |
856 | public function getModuleConfigPath($module) |
857 | { |
858 | $configPath = APPLICATION_PATH . "/module/$module/config/module.config.php"; |
859 | if (!file_exists($configPath)) { |
860 | throw new \Exception("Cannot find $configPath"); |
861 | } |
862 | return $configPath; |
863 | } |
864 | |
865 | /** |
866 | * Write a module configuration. |
867 | * |
868 | * @param string $configPath Path to write to |
869 | * @param string $config Configuration array to write |
870 | * |
871 | * @return void |
872 | * @throws \Exception |
873 | */ |
874 | public function writeModuleConfig($configPath, $config) |
875 | { |
876 | $generator = FileGenerator::fromArray( |
877 | [ |
878 | 'body' => 'return ' . var_export($config, true) . ';', |
879 | ] |
880 | ); |
881 | if (!file_put_contents($configPath, $generator->generate())) { |
882 | throw new \Exception("Cannot write to $configPath"); |
883 | } |
884 | $this->writeln("Successfully updated $configPath"); |
885 | } |
886 | |
887 | /** |
888 | * Apply a single setting to a configuration array. |
889 | * |
890 | * @param array $path Representation of path in config array |
891 | * @param string|array $setting New setting to write into config |
892 | * @param array $config Configuration array (passed by reference) |
893 | * |
894 | * @return void |
895 | */ |
896 | protected function applySettingToConfig( |
897 | array $path, |
898 | $setting, |
899 | array &$config |
900 | ) { |
901 | $current = & $config; |
902 | $finalStep = array_pop($path); |
903 | foreach ($path as $step) { |
904 | if (!is_array($current)) { |
905 | throw new \Exception('Unexpected non-array: ' . $current); |
906 | } |
907 | if (!isset($current[$step])) { |
908 | $current[$step] = []; |
909 | } |
910 | $current = & $current[$step]; |
911 | } |
912 | if (!is_array($current)) { |
913 | throw new \Exception('Unexpected non-array: ' . $current); |
914 | } |
915 | $current[$finalStep] = $setting; |
916 | } |
917 | |
918 | /** |
919 | * Update the configuration of a target module with multiple settings. |
920 | * |
921 | * @param array $newValues An array of arrays containing 'path' and 'setting' |
922 | * keys to specify changes to the configuration. |
923 | * @param string $module Module in which to write the configuration |
924 | * @param bool $backup Should we back up the existing config? |
925 | * |
926 | * @return void |
927 | * @throws \Exception |
928 | */ |
929 | protected function writeNewConfigs( |
930 | array $newValues, |
931 | string $module, |
932 | bool $backup = true |
933 | ) { |
934 | // Create backup of configuration |
935 | $configPath = $this->getModuleConfigPath($module); |
936 | if ($backup) { |
937 | $this->backUpFile($configPath); |
938 | } |
939 | |
940 | $config = include $configPath; |
941 | foreach ($newValues as $current) { |
942 | $this->applySettingToConfig( |
943 | $current['path'], |
944 | $current['setting'], |
945 | $config |
946 | ); |
947 | } |
948 | |
949 | // Write updated configuration |
950 | $this->writeModuleConfig($configPath, $config); |
951 | } |
952 | |
953 | /** |
954 | * Update the configuration of a target module with a single setting. |
955 | * |
956 | * @param array $path Representation of path in config array |
957 | * @param string|array $setting New setting to write into config |
958 | * @param string $module Module in which to write the configuration |
959 | * @param bool $backup Should we back up the existing config? |
960 | * |
961 | * @return void |
962 | * @throws \Exception |
963 | */ |
964 | protected function writeNewConfig($path, $setting, $module, $backup = true) |
965 | { |
966 | $this->writeNewConfigs([compact('path', 'setting')], $module, $backup); |
967 | } |
968 | |
969 | /** |
970 | * Retrieve a value from the application configuration (or return false |
971 | * if the path is not found). |
972 | * |
973 | * @param array $path Path to walk through configuration |
974 | * |
975 | * @return mixed |
976 | */ |
977 | protected function retrieveConfig(array $path) |
978 | { |
979 | $config = $this->config; |
980 | foreach ($path as $part) { |
981 | if (!isset($config[$part])) { |
982 | return false; |
983 | } |
984 | $config = $config[$part]; |
985 | } |
986 | return $config; |
987 | } |
988 | } |