From fc2c7731620399864043f282226dd98fa6d7ce77 Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Wed, 7 Feb 2018 09:30:40 -0500
Subject: [PATCH] Add extendclass generator to replace extendservice.

---
 module/VuFindConsole/Module.php               |   1 +
 module/VuFindConsole/config/module.config.php |   1 +
 .../Controller/GenerateController.php         |  36 ++++++
 .../Generator/GeneratorTools.php              | 108 +++++++++++++++++-
 4 files changed, 144 insertions(+), 2 deletions(-)

diff --git a/module/VuFindConsole/Module.php b/module/VuFindConsole/Module.php
index 09d67f3c707..6c4b4c7fa03 100644
--- a/module/VuFindConsole/Module.php
+++ b/module/VuFindConsole/Module.php
@@ -99,6 +99,7 @@ class Module implements \Zend\ModuleManager\Feature\ConsoleUsageProviderInterfac
         return [
             'compile theme' => 'Flatten a theme hierarchy for improved performance',
             'generate dynamicroute' => 'Add a dynamic route',
+            'generate extendclass' => 'Subclass a service, w/ lookup by class name',
             'generate extendservice' => 'Override a service with a new child class',
             'generate nontabrecordaction' => 'Add routes for non-tab record action',
             'generate recordroute' => 'Add a record route',
diff --git a/module/VuFindConsole/config/module.config.php b/module/VuFindConsole/config/module.config.php
index 2a8482527b4..6debc0e4420 100644
--- a/module/VuFindConsole/config/module.config.php
+++ b/module/VuFindConsole/config/module.config.php
@@ -52,6 +52,7 @@ $config = [
 $routes = [
     'compile/theme' => 'compile theme [--force] [<source>] [<target>]',
     'generate/dynamicroute' => 'generate dynamicroute [<name>] [<newController>] [<newAction>] [<module>]',
+    'generate/extendclass' => 'generate extendclass [<class>] [<target>]',
     'generate/extendservice' => 'generate extendservice [<source>] [<target>]',
     'generate/nontabrecordaction' => 'generate nontabrecordaction [<newAction>] [<module>]',
     'generate/recordroute' => 'generate recordroute [<base>] [<newController>] [<module>]',
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index d4945d27f97..aff4b6f8979 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -87,6 +87,42 @@ class GenerateController extends AbstractBase
         return $this->getSuccessResponse();
     }
 
+    /**
+     * Extend an existing class
+     *
+     * @return \Zend\Console\Response
+     */
+    public function extendclassAction()
+    {
+        // Display help message if parameters missing:
+        $request = $this->getRequest();
+        $class = $request->getParam('class');
+        $target = $request->getParam('target');
+        if (empty($class) || empty($target)) {
+            Console::writeLine(
+                'Usage: ' . $request->getScriptName() . ' generate extendclass'
+                . ' [class_name] [target_module]'
+            );
+            Console::writeLine(
+                "\tclass_name - the name of the class you wish to extend"
+            );
+            Console::writeLine(
+                "\ttarget_module - the module where the new class will be generated"
+            );
+            return $this->getFailureResponse();
+        }
+
+        try {
+            $this->getGeneratorTools()
+                ->extendClass($this->serviceLocator, $class, $target);
+        } catch (\Exception $e) {
+            Console::writeLine($e->getMessage());
+            return $this->getFailureResponse();
+        }
+
+        return $this->getSuccessResponse();
+    }
+
     /**
      * Extend an existing service
      *
diff --git a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
index 35bc069300a..9c22031c563 100644
--- a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
+++ b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
@@ -27,6 +27,7 @@
  */
 namespace VuFindConsole\Generator;
 
+use Interop\Container\ContainerInterface;
 use Zend\Code\Generator\ClassGenerator;
 use Zend\Code\Generator\FileGenerator;
 use Zend\Code\Generator\MethodGenerator;
@@ -61,6 +62,106 @@ class GeneratorTools
         $this->config = $config;
     }
 
+    /**
+     * Extend a class defined somewhere in the service manager or its child
+     * plugin managers.
+     *
+     * @param ContainerInterface $container Service manager
+     * @param string             $class     Class name to extend
+     * @param string             $target    Target module in which to create new
+     * service
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public function extendClass(ContainerInterface $container, $class, $target)
+    {
+        // Set things up differently depending on whether this is a top-level
+        // service or a class in a plugin manager.
+        if ($container->has($class)) {
+            $factory = $this->getFactoryFromContainer($container, $class);
+            $configPath = ['service_manager'];
+        } else {
+            $pm = $this->getPluginManagerContainingClass($container, $class);
+            $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory();
+            $pmKey = $apmFactory->getConfigKey(get_class($pm));
+            $factory = $this->getFactoryFromContainer($pm, $class);
+            $configPath = ['vufind', 'plugin_managers', $pmKey];
+        }
+
+        // No factory found? Throw an error!
+        if (!$factory) {
+            throw new \Exception('Could not find factory for ' . $class);
+        }
+
+        // Create the custom subclass.
+        $newClass = $this->createSubclassInModule($class, $target);
+
+        // Finalize the local module configuration -- create a factory for the
+        // new class, and set up the new class as an alias for the old class.
+        $factoryPath = array_merge($configPath, ['factories', $newClass]);
+        $this->writeNewConfig($factoryPath, $factory, $target);
+        $aliasPath = array_merge($configPath, ['aliases', $class]);
+        // Don't back up the config twice -- the first backup from the previous
+        // write operation is sufficient.
+        $this->writeNewConfig($aliasPath, $newClass, $target, false);
+
+        return true;
+    }
+
+    /**
+     * Get a list of factories in the provided container.
+     *
+     * @param ContainerInterface $container Container to inspect
+     *
+     * @return array
+     */
+    protected function getAllFactoriesFromContainer(ContainerInterface $container)
+    {
+        // There is no "getFactories" method, so we need to use reflection:
+        $reflectionProperty = new \ReflectionProperty($container, 'factories');
+        $reflectionProperty->setAccessible(true);
+        return $reflectionProperty->getValue($container);
+    }
+
+    /**
+     * Get a factory from the provided container (or null if undefined).
+     *
+     * @param ContainerInterface $container Container to inspect
+     * @param string             $class     Class whose factory we want
+     *
+     * @return string
+     */
+    protected function getFactoryFromContainer(ContainerInterface $container, $class)
+    {
+        $factories = $this->getAllFactoriesFromContainer($container);
+        return isset($factories[$class]) ? $factories[$class] : null;
+    }
+
+    /**
+     * Search all plugin managers for one containing the requested class (or return
+     * null if none found).
+     *
+     * @param ContainerInterface $container Service manager
+     * @param string             $class     Class to search for
+     *
+     * @return ContainerInterface
+     */
+    protected function getPluginManagerContainingClass(ContainerInterface $container,
+        $class
+    ) {
+        $factories = $this->getAllFactoriesFromContainer($container);
+        foreach (array_keys($factories) as $service) {
+            if (substr($service, -13) == 'PluginManager') {
+                $pm = $container->get($service);
+                if (null !== $this->getFactoryFromContainer($pm, $class)) {
+                    return $pm;
+                }
+            }
+        }
+        return null;
+    }
+
     /**
      * Extend a service defined in module.config.php.
      *
@@ -376,15 +477,18 @@ class GeneratorTools
      * @param array  $path    Representation of path in config array
      * @param string $setting New setting to write into config
      * @param string $module  Module in which to write the configuration
+     * @param bool   $backup  Should we back up the existing config?
      *
      * @return void
      * @throws \Exception
      */
-    protected function writeNewConfig($path, $setting, $module)
+    protected function writeNewConfig($path, $setting, $module, $backup  = true)
     {
         // Create backup of configuration
         $configPath = $this->getModuleConfigPath($module);
-        $this->backUpFile($configPath);
+        if ($backup) {
+            $this->backUpFile($configPath);
+        }
 
         $config = include $configPath;
         $current = & $config;
-- 
GitLab