Skip to content
Snippets Groups Projects
Commit e1edb3b6 authored by Demian Katz's avatar Demian Katz Committed by Robert Lange
Browse files

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.
parent 7a036cea
No related merge requests found
......@@ -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;
......
......@@ -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 = [
......
......@@ -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);
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment