From e1edb3b69cabdf4ed3c080c2f8ea263159fcc9d6 Mon Sep 17 00:00:00 2001 From: Demian Katz <demian.katz@villanova.edu> Date: Tue, 12 May 2020 08:32:17 -0400 Subject: [PATCH] Improve plugin generator. (#1573) - Add support for generating controllers and controller plugins. - Add support for generating plugin types from other VuFind modules (e.g. VuFindConsole Commands). - Add option to create top-level services. - Improved factory generation (use $requestedName instead of hard-coded class name). - Better aliases for controllers. --- .../Command/Generate/PluginCommand.php | 12 +- .../Generator/GeneratorTools.php | 214 +++++++++++++++--- .../Command/Generate/PluginCommandTest.php | 9 +- 3 files changed, 204 insertions(+), 31 deletions(-) diff --git a/module/VuFindConsole/src/VuFindConsole/Command/Generate/PluginCommand.php b/module/VuFindConsole/src/VuFindConsole/Command/Generate/PluginCommand.php index ec5ad658913..dfd4aaa0592 100644 --- a/module/VuFindConsole/src/VuFindConsole/Command/Generate/PluginCommand.php +++ b/module/VuFindConsole/src/VuFindConsole/Command/Generate/PluginCommand.php @@ -29,6 +29,7 @@ namespace VuFindConsole\Command\Generate; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -67,6 +68,12 @@ class PluginCommand extends AbstractContainerAwareCommand 'factory', InputArgument::OPTIONAL, 'an existing factory to use (omit to generate a new one)' + )->addOption( + 'top-level', + null, + InputOption::VALUE_NONE, + 'when set, create the plugin as a top-level service instead of' + . ' inside a plugin manager' ); } @@ -82,9 +89,10 @@ class PluginCommand extends AbstractContainerAwareCommand { $class = $input->getArgument('class_name'); $factory = $input->getArgument('factory'); + $topLevel = $input->getOption('top-level'); try { - $this->generatorTools->setOutputInterface($output); - $this->generatorTools->createPlugin($this->container, $class, $factory); + $this->generatorTools->setOutputInterface($output) + ->createPlugin($this->container, $class, $factory, $topLevel); } catch (\Exception $e) { $output->writeln($e->getMessage()); return 1; diff --git a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php index 0138401b883..ee00a5199fa 100644 --- a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php +++ b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php @@ -63,27 +63,194 @@ class GeneratorTools $this->config = $config; } + /** + * Determine a plugin manager name within the specified namespace. + * + * @param array $classParts Exploded class name array + * @param string $namespace Namespace to try for plugin manager + * + * @return string + */ + protected function getPluginManagerForNamespace($classParts, $namespace) + { + $classParts[0] = $namespace; + $classParts[count($classParts) - 1] = 'PluginManager'; + return implode('\\', $classParts); + } + + /** + * Get a list of VuFind modules (only those with names beginning with VuFind, + * and not including the core VuFind module itself). + * + * @return array + */ + protected function getVuFindExtendedModules() + { + $moduleDir = __DIR__ . '/../../../../'; + $handle = opendir($moduleDir); + $results = []; + while ($line = readdir($handle)) { + if (substr($line, 0, 6) === 'VuFind' && strlen($line) > 6) { + $results[] = $line; + } + } + closedir($handle); + return $results; + } + + /** + * Given a class name exploded into an array, figure out the appropriate plugin + * manager to use. + * + * @param array $classParts Exploded class name array + * + * @return string + */ + protected function getPluginManagerFromExplodedClassName($classParts) + { + $pmClass = $this->getPluginManagerForNamespace($classParts, 'VuFind'); + // Special cases: no such service; use framework core services instead: + if ($pmClass === 'VuFind\Controller\PluginManager') { + return 'ControllerManager'; + } + if ($pmClass === 'VuFind\Controller\Plugin\PluginManager') { + return \Laminas\Mvc\Controller\PluginManager::class; + } + // Special case: no such service; check other modules: + if (!class_exists($pmClass)) { + foreach ($this->getVuFindExtendedModules() as $module) { + $pmClass = $this->getPluginManagerForNamespace($classParts, $module); + if (class_exists($pmClass)) { + break; + } + } + } + return $pmClass; + } + + /** + * Given a class name exploded into an array, figure out the appropriate short + * name to use as an alias in the service manager configuration. + * + * @param array $classParts Exploded class name array + * + * @return string + */ + protected function getShortNameFromExplodedClassName($classParts) + { + $shortName = array_pop($classParts); + // Special case: controllers use shortened aliases + if (($classParts[1] ?? '') === 'Controller') { + return preg_replace('/Controller$/', '', $shortName); + } + return strtolower($shortName); + } + + /** + * Given a plugin manager object, return the interface plugins of that type must + * implement. + * + * @param ContainerInterface $pm Plugin manager + * + * @return string + */ + protected function getExpectedInterfaceFromPluginManager($pm) + { + // Special case: controllers + if ($pm instanceof \Laminas\Mvc\Controller\ControllerManager) { + return \VuFind\Controller\AbstractBase::class; + } + + // Special case: controller plugins: + if ($pm instanceof \Laminas\Mvc\Controller\PluginManager) { + return \Laminas\Mvc\Controller\Plugin\AbstractPlugin::class; + } + + // Default case: look it up: + if (!method_exists($pm, 'getExpectedInterface')) { + return null; + } + + // Force getExpectedInterface() to be public so we can read it: + $reflectionMethod = new \ReflectionMethod($pm, 'getExpectedInterface'); + $reflectionMethod->setAccessible(true); + return $reflectionMethod->invoke($pm); + } + + /** + * Given a plugin manager class name, return the configuration path for that + * plugin manager. + * + * @param string $class Class name + * + * @return array + */ + protected function getConfigPathForClass($class) + { + // Special case: controller + if ($class === \Laminas\Mvc\Controller\ControllerManager::class) { + return ['controllers']; + } elseif ($class == \Laminas\Mvc\Controller\PluginManager::class) { + return ['controller_plugins']; + } elseif ($class == \Laminas\ServiceManager\ServiceManager::class) { + return ['service_manager']; + } + // Default case: VuFind internal plugin manager + $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory(); + $pmKey = $apmFactory->getConfigKey($class); + return ['vufind', 'plugin_managers', $pmKey]; + } + + /** + * Given appropriate inputs, figure out which plugin manager or service manager + * to use during plugin generation. + * + * @param ContainerInterface $container Service manager + * @param array $classParts Exploded class name array + * @param bool $topLevelService Set to true to build a service + * in the top-level container rather than a plugin in a subsidiary plugin manager + * + * @return ContainerInterface + */ + protected function getPluginManagerForClassParts($container, $classParts, + $topLevelService + ) { + // Special case -- short-circuit for top-level service: + if ($topLevelService) { + return $container; + } + $pmClass = $this->getPluginManagerFromExplodedClassName($classParts); + if (!$container->has($pmClass)) { + throw new \Exception( + 'Cannot find expected plugin manager: ' . $pmClass . "\n" + . 'You can use the --top-level option if you wish to create' + . ' a top-level service.' + ); + } + return $container->get($pmClass); + } + /** * Create a plugin class. * - * @param ContainerInterface $container Service manager - * @param string $class Class name to create - * @param string $factory Existing factory to use (null to + * @param ContainerInterface $container Service manager + * @param string $class Class name to create + * @param string $factory Existing factory to use (null to * generate a new one) + * @param bool $topLevelService Set to true to build a service + * in the top-level container rather than a plugin in a subsidiary plugin manager * * @return bool * @throws \Exception */ public function createPlugin(ContainerInterface $container, $class, - $factory = null + $factory = null, $topLevelService = false ) { // Derive some key bits of information from the new class name: $classParts = explode('\\', $class); $module = $classParts[0]; - $shortName = strtolower(array_pop($classParts)); - $classParts[0] = 'VuFind'; - $classParts[] = 'PluginManager'; - $pmClass = implode('\\', $classParts); + $shortName = $this->getShortNameFromExplodedClassName($classParts); + // Set a flag for whether to generate a factory, and create class name // if necessary. If existing factory specified, ensure it really exists. if ($generateFactory = empty($factory)) { @@ -93,20 +260,10 @@ class GeneratorTools } // Figure out further information based on the plugin manager: - if (!$container->has($pmClass)) { - throw new \Exception('Cannot find expected plugin manager: ' . $pmClass); - } - $pm = $container->get($pmClass); - if (!method_exists($pm, 'getExpectedInterface')) { - throw new \Exception( - $pmClass . ' does not implement getExpectedInterface!' - ); - } - - // Force getExpectedInterface() to be public so we can read it: - $reflectionMethod = new \ReflectionMethod($pm, 'getExpectedInterface'); - $reflectionMethod->setAccessible(true); - $interface = $reflectionMethod->invoke($pm); + $pm = $this->getPluginManagerForClassParts( + $container, $classParts, $topLevelService + ); + $interface = $this->getExpectedInterfaceFromPluginManager($pm); // Figure out whether the plugin requirement is an interface or a // parent class so we can create the right thing.... @@ -117,9 +274,7 @@ class GeneratorTools $parent = $interface; $interfaces = []; } - $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory(); - $pmKey = $apmFactory->getConfigKey(get_class($pm)); - $configPath = ['vufind', 'plugin_managers', $pmKey]; + $configPath = $this->getConfigPathForClass(get_class($pm)); // Generate the classes and configuration: $this->createClassInModule($class, $module, $parent, $interfaces); @@ -132,6 +287,13 @@ class GeneratorTools // Don't back up the config twice -- the first backup from the previous // write operation is sufficient. $this->writeNewConfig($aliasPath, $class, $module, false); + // Add extra lowercase alias if necessary: + if (strtolower($shortName) != $shortName) { + $lowerAliasPath = array_merge( + $configPath, ['aliases', strtolower($shortName)] + ); + $this->writeNewConfig($lowerAliasPath, $class, $module, false); + } return true; } @@ -154,7 +316,7 @@ class GeneratorTools $method = MethodGenerator::fromArray( [ 'name' => '__invoke', - 'body' => 'return new \\' . $class . '();', + 'body' => 'return new $requestedName();', ] ); $param1 = [ diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Generate/PluginCommandTest.php b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Generate/PluginCommandTest.php index 27f54b4571e..91a50b3ce1a 100644 --- a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Generate/PluginCommandTest.php +++ b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Generate/PluginCommandTest.php @@ -75,7 +75,8 @@ class PluginCommandTest extends \PHPUnit\Framework\TestCase $tools = $this->getMockGeneratorTools( ['setOutputInterface', 'createPlugin'] ); - $tools->expects($this->once())->method('setOutputInterface'); + $tools->expects($this->once())->method('setOutputInterface') + ->will($this->returnValue($tools)); $tools->expects($this->once())->method('createPlugin') ->with( $this->equalTo($container), @@ -99,7 +100,8 @@ class PluginCommandTest extends \PHPUnit\Framework\TestCase $tools = $this->getMockGeneratorTools( ['setOutputInterface', 'createPlugin'] ); - $tools->expects($this->once())->method('setOutputInterface'); + $tools->expects($this->once())->method('setOutputInterface') + ->will($this->returnValue($tools)); $tools->expects($this->once())->method('createPlugin') ->with( $this->equalTo($container), @@ -125,7 +127,8 @@ class PluginCommandTest extends \PHPUnit\Framework\TestCase $tools = $this->getMockGeneratorTools( ['createPlugin', 'setOutputInterface'] ); - $tools->expects($this->once())->method('setOutputInterface'); + $tools->expects($this->once())->method('setOutputInterface') + ->will($this->returnValue($tools)); $tools->expects($this->once())->method('createPlugin') ->will($this->throwException(new \Exception('Foo!'))); $command = new PluginCommand($tools, $container); -- GitLab