From 295d7278713c6f025c5306eb691f99dcb2502454 Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Fri, 13 Feb 2015 12:41:42 -0500
Subject: [PATCH] Added generator support for extending factory-created
 services. - Progress on VUFIND-1035.

---
 .../Controller/GenerateController.php         | 174 +++++++++++++++---
 1 file changed, 152 insertions(+), 22 deletions(-)

diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index 2fb40088598..ba5d86ba2f4 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -27,7 +27,9 @@
  */
 namespace VuFindConsole\Controller;
 use Zend\Code\Generator\ClassGenerator;
+use Zend\Code\Generator\MethodGenerator;
 use Zend\Code\Generator\FileGenerator;
+use Zend\Code\Reflection\ClassReflection;
 use Zend\Console\Console;
 
 /**
@@ -122,52 +124,167 @@ class GenerateController extends AbstractBase
      */
     protected function cloneFactory($factory, $module)
     {
-        throw new \Exception('Factories not supported yet.');
+        // Make sure we can figure out how to handle the factory; it should
+        // either be a [controller, method] array or a "controller::method"
+        // string; anything else will cause a problem.
+        $parts = is_string($factory) ? explode('::', $factory) : $factory;
+        if (!is_array($parts) || count($parts) != 2 || !class_exists($parts[0])
+            || !method_exists($parts[0], $parts[1])
+        ) {
+            throw new \Exception('Unexpected factory configuration format.');
+        }
+        list($factoryClass, $factoryMethod) = $parts;
+        $newFactoryClass = $this->generateLocalClassName($factoryClass, $module);
+        if (!class_exists($newFactoryClass)) {
+            $this->createClassInModule($newFactoryClass, $module);
+            $skipBackup = true;
+        } else {
+            $skipBackup = false;
+        }
+        if (method_exists($newFactoryClass, $factoryMethod)) {
+            throw new \Exception("$newFactoryClass::$factoryMethod already exists.");
+        }
+
+        $oldReflection = new ClassReflection($factoryClass);
+        $newReflection = new ClassReflection($newFactoryClass);
+
+        $generator = ClassGenerator::fromReflection($newReflection);
+        $method = MethodGenerator::fromReflection(
+            $oldReflection->getMethod($factoryMethod)
+        );
+        $this->createServiceClassAndUpdateFactory($method, $module);
+        $generator->addMethodFromGenerator($method);
+        $this->writeClass($generator, $module, true, $skipBackup);
+
+        return $newFactoryClass . '::' . $factoryMethod;
     }
 
     /**
-     * Extend a specified class within a specified module. Return the name of
-     * the new subclass.
+     * Given a factory method, extend the class being constructed and create
+     * a new factory for the subclass.
      *
-     * @param string $class  Name of class to extend
-     * @param string $module Module in which to create the new invokable
+     * @param MethodGenerator $method Method to modify
+     * @param string          $module Module in which to make changes
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function createServiceClassAndUpdateFactory(MethodGenerator $method,
+        $module
+    ) {
+        $body = $method->getBody();
+        $regex = '/new\s+([\w\\\\]*)\s*\(/m';
+        preg_match_all($regex, $body, $matches);
+        $classNames = $matches[1];
+        $count = count($classNames);
+        if ($count != 1) {
+            throw new \Exception("Found $count class names; expected 1.");
+        }
+        $className = $classNames[0];
+        $newClass = $this->createSubclassInModule($className, $module);
+        $body = preg_replace(
+            '/new\s+' . addslashes($className) . '\s*\(/m',
+            'new \\' . $newClass . '(',
+            $body
+        );
+        $method->setBody($body);
+    }
+
+    /**
+     * Determine the name of a local replacement class within the specified
+     * module.
+     *
+     * @param string $class  Name of class to extend/replace
+     * @param string $module Module in which to create the new class
      *
      * @return string
      * @throws \Exception
      */
-    protected function createSubclassInModule($class, $module)
+    protected function generateLocalClassName($class, $module)
     {
         // Determine the name of the new class by exploding the old class and
         // replacing the namespace:
-        $parts = explode('\\', $class);
+        $parts = explode('\\', trim($class, '\\'));
         if (count($parts) < 2) {
             throw new \Exception('Expected a namespaced class; found ' . $class);
         }
         $parts[0] = $module;
-        $newClass = implode('\\', $parts);
+        return implode('\\', $parts);
+    }
 
+    /**
+     * Extend a specified class within a specified module. Return the name of
+     * the new subclass.
+     *
+     * @param string $class  Name of class to create
+     * @param string $module Module in which to create the new class
+     * @param string $parent Parent class (null for no parent)
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function createClassInModule($class, $module, $parent = null)
+    {
+        $generator = new ClassGenerator($class, null, null, $parent);
+        return $this->writeClass($generator, $module);
+    }
+
+    /**
+     * Write a class to disk.
+     *
+     * @param ClassGenerator $classGenerator Representation of class to write
+     * @param string         $module         Module in which to write class
+     * @param bool           $allowOverwrite Allow overwrite of existing file?
+     * @param bool           $skipBackup     Should we skip backing up the file?
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function writeClass(ClassGenerator $classGenerator, $module,
+        $allowOverwrite = false, $skipBackup = false
+    ) {
         // Use the class name parts from the previous step to determine a path
         // and filename, then create the new path.
-        $filename = array_pop($parts) . '.php';
+        $parts = explode('\\', $classGenerator->getNamespaceName());
         array_unshift($parts, 'module', $module, 'src');
         $this->createTree($parts);
 
         // Generate the new class:
-        $generator = FileGenerator::fromArray(
-            [
-                'classes' => [new ClassGenerator($newClass, null, null, "\\$class")]
-            ]
-        );
+        $generator = FileGenerator::fromArray(['classes' => [$classGenerator]]);
+        $filename = $classGenerator->getName() . '.php';
         $fullPath = APPLICATION_PATH . '/' . implode('/', $parts) . '/' . $filename;
         if (file_exists($fullPath)) {
-            throw new \Exception("$fullPath already exists.");
+            if ($allowOverwrite) {
+                if (!$skipBackup) {
+                    $this->backUpFile($fullPath);
+                }
+            } else {
+                throw new \Exception("$fullPath already exists.");
+            }
         }
         if (!file_put_contents($fullPath, $generator->generate())) {
             throw new \Exception("Problem writing to $fullPath.");
         }
-        Console::writeLine("Generated new file: $fullPath");
+        Console::writeLine("Saved file: $fullPath");
+    }
 
-        // Send back the name of the new class:
+    /**
+     * Extend a specified class within a specified module. Return the name of
+     * the new subclass.
+     *
+     * @param string $class  Name of class to extend
+     * @param string $module Module in which to create the new class
+     *
+     * @return string
+     * @throws \Exception
+     */
+    protected function createSubclassInModule($class, $module)
+    {
+        // Normalize leading backslashes; in some contexts we will
+        // have them and in others we may not.
+        $class = trim($class, '\\');
+        $newClass = $this->generateLocalClassName($class, $module);
+        $this->createClassInModule($newClass, $module, "\\$class");
         return $newClass;
     }
 
@@ -196,6 +313,23 @@ class GenerateController extends AbstractBase
         }
     }
 
+    /**
+     * Create a backup of a file.
+     *
+     * @param string $filename File to back up
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function backUpFile($filename)
+    {
+        $backup = $filename . '.' . time() . '.bak';
+        if (!copy($filename, $backup)) {
+            throw new \Exception("Problem generating backup file: $backup");
+        }
+        Console::writeLine("Created backup: $backup");
+    }
+
     /**
      * Update the configuration of a target module.
      *
@@ -215,11 +349,7 @@ class GenerateController extends AbstractBase
         }
 
         // Create backup of configuration
-        $backup = $configPath . '.' . time() . '.bak';
-        if (!copy($configPath, $backup)) {
-            throw new \Exception("Problem generating backup file: $backup");
-        }
-        Console::writeLine("Created configuration backup: $backup");
+        $this->backUpFile($configPath);
 
         $config = require $configPath;
         $current = & $config;
-- 
GitLab