From bd724faf5f49e278277fbc8150f473224dec24c1 Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Thu, 26 Apr 2018 11:44:17 -0400
Subject: [PATCH]  Refactor class-based template-rendering to a shared base
 class.  (#1172)

---
 .../AbstractClassBasedTemplateRenderer.php    | 119 ++++++++++++++++++
 .../src/VuFind/View/Helper/Root/Auth.php      |  48 +------
 .../src/VuFind/View/Helper/Root/Recommend.php |  40 +-----
 .../src/VuFind/View/Helper/Root/Record.php    |  42 +------
 .../src/VuFind/View/Helper/Root/Related.php   |  40 +-----
 .../View/Helper/Root/RecordTest.php           |  30 ++---
 6 files changed, 151 insertions(+), 168 deletions(-)
 create mode 100644 module/VuFind/src/VuFind/View/Helper/Root/AbstractClassBasedTemplateRenderer.php

diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AbstractClassBasedTemplateRenderer.php b/module/VuFind/src/VuFind/View/Helper/Root/AbstractClassBasedTemplateRenderer.php
new file mode 100644
index 00000000000..0d89815f9fe
--- /dev/null
+++ b/module/VuFind/src/VuFind/View/Helper/Root/AbstractClassBasedTemplateRenderer.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Abstract base class for helpers that render a template based on a class name.
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\View\Helper\Root;
+
+use Zend\View\Exception\RuntimeException;
+use Zend\View\Helper\AbstractHelper;
+
+/**
+ * Authentication view helper
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+abstract class AbstractClassBasedTemplateRenderer extends AbstractHelper
+{
+    /**
+     * Recursively locate and render a template that matches the provided class
+     * name (or one of its parent classes); throw an exception if no match is
+     * found.
+     *
+     * @param string $template     Template path (with %s as class name placeholder)
+     * @param string $className    Name of class to apply to template.
+     * @param string $topClassName Top-level parent class of $className (or null
+     * if $className is already the top level; used for recursion only).
+     *
+     * @return string
+     * @throws RuntimeException
+     */
+    protected function resolveClassTemplate($template, $className,
+        $topClassName = null
+    ) {
+        // If the template resolves, we can render it!
+        $templateWithClass = sprintf($template, $this->getBriefClass($className));
+        if ($this->getView()->resolver()->resolve($templateWithClass)) {
+            return $this->getView()->render($templateWithClass);
+        }
+
+        // If the template doesn't resolve, let's see if we can inherit a
+        // template from a parent class:
+        $parentClass = get_parent_class($className);
+        if (empty($parentClass)) {
+            // No more parent classes left to try?  Throw an exception!
+            throw new RuntimeException(
+                'Cannot find ' . $templateWithClass . ' template for class: '
+                . ($topClassName ?? $className)
+            );
+        }
+
+        // Recurse until we find a template or run out of parents...
+        return $this->resolveClassTemplate(
+            $template, $parentClass, $topClassName ?? $className
+        );
+    }
+
+    /**
+     * Render a template associated with the provided class name, applying to
+     * specified context variables.
+     *
+     * @param string $template  Template path (with %s as class name placeholder)
+     * @param string $className Name of class to apply to template.
+     * @param array  $context   Context for rendering template
+     *
+     * @return string
+     */
+    protected function renderClassTemplate($template, $className, $context = [])
+    {
+        // Set up the needed context in the view:
+        $contextHelper = $this->getView()->plugin('context');
+        $oldContext = $contextHelper($this->getView())->apply($context);
+
+        // Render the template for the current class:
+        $html = $this->resolveClassTemplate($template, $className);
+
+        // Restore the original context before returning the result:
+        $contextHelper($this->getView())->restore($oldContext);
+        return $html;
+    }
+
+    /**
+     * Helper to grab the end of the class name
+     *
+     * @param string $className Class name to abbreviate
+     *
+     * @return string
+     */
+    protected function getBriefClass($className)
+    {
+        $classParts = explode('\\', $className);
+        return array_pop($classParts);
+    }
+}
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
index 901f5dffcda..137385bd357 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
@@ -28,7 +28,6 @@
 namespace VuFind\View\Helper\Root;
 
 use VuFind\Exception\ILS as ILSException;
-use Zend\View\Exception\RuntimeException;
 
 /**
  * Authentication view helper
@@ -39,7 +38,7 @@ use Zend\View\Exception\RuntimeException;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class Auth extends \Zend\View\Helper\AbstractHelper
+class Auth extends AbstractClassBasedTemplateRenderer
 {
     /**
      * Authentication manager
@@ -80,37 +79,9 @@ class Auth extends \Zend\View\Helper\AbstractHelper
     {
         // Get the current auth module's class name
         $className = $this->getManager()->getAuthClassForTemplateRendering();
-
-        // Set up the needed context in the view:
-        $contextHelper = $this->getView()->plugin('context');
+        $template = 'Auth/%s/' . $name;
         $context['topClass'] = $this->getBriefClass($className);
-        $oldContext = $contextHelper($this->getView())->apply($context);
-
-        // Start a loop in case we need to use a parent class' name to find the
-        // appropriate template.
-        $topClassName = $className; // for error message
-        $resolver = $this->getView()->resolver();
-        while (true) {
-            // Guess the template name for the current class:
-            $template = 'Auth/' . $this->getBriefClass($className) . '/' . $name;
-            if ($resolver->resolve($template)) {
-                // Try to render the template....
-                $html = $this->getView()->render($template);
-                $contextHelper($this->getView())->restore($oldContext);
-                return $html;
-            } else {
-                // If the template doesn't exist, let's see if we can inherit a
-                // template from a parent class:
-                $className = get_parent_class($className);
-                if (empty($className)) {
-                    // No more parent classes left to try?  Throw an exception!
-                    throw new RuntimeException(
-                        'Cannot find ' . $name . ' template for auth module: '
-                        . $topClassName
-                    );
-                }
-            }
-        }
+        return $this->renderClassTemplate($template, $className, $context);
     }
 
     /**
@@ -196,19 +167,6 @@ class Auth extends \Zend\View\Helper\AbstractHelper
         return $this->renderTemplate('logindesc.phtml', $context);
     }
 
-    /**
-     * Helper to grab the end of the class name
-     *
-     * @param string $className Class name to abbreviate
-     *
-     * @return string
-     */
-    protected function getBriefClass($className)
-    {
-        $classParts = explode('\\', $className);
-        return array_pop($classParts);
-    }
-
     /**
      * Render the new password form template.
      *
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php b/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
index 72442eec473..e1f7dd3bf33 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
@@ -27,9 +27,6 @@
  */
 namespace VuFind\View\Helper\Root;
 
-use Zend\View\Exception\RuntimeException;
-use Zend\View\Helper\AbstractHelper;
-
 /**
  * Recommendation module view helper
  *
@@ -39,7 +36,7 @@ use Zend\View\Helper\AbstractHelper;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class Recommend extends AbstractHelper
+class Recommend extends AbstractClassBasedTemplateRenderer
 {
     /**
      * Render the output of a recommendation module.
@@ -51,38 +48,9 @@ class Recommend extends AbstractHelper
      */
     public function __invoke($recommend)
     {
-        // Set up the rendering context:
-        $contextHelper = $this->getView()->plugin('context');
-        $oldContext = $contextHelper($this->getView())->apply(
-            ['recommend' => $recommend]
-        );
-
-        // Get the current recommendation module's class name, then start a loop
-        // in case we need to use a parent class' name to find the appropriate
-        // template.
+        $template = 'Recommend/%s.phtml';
         $className = get_class($recommend);
-        $resolver = $this->getView()->resolver();
-        while (true) {
-            // Guess the template name for the current class:
-            $classParts = explode('\\', $className);
-            $template = 'Recommend/' . array_pop($classParts) . '.phtml';
-            if ($resolver->resolve($template)) {
-                // Try to render the template....
-                $html = $this->getView()->render($template);
-                $contextHelper($this->getView())->restore($oldContext);
-                return $html;
-            } else {
-                // If the template doesn't exist, let's see if we can inherit a
-                // template from a parent recommendation class:
-                $className = get_parent_class($className);
-                if (empty($className)) {
-                    // No more parent classes left to try?  Throw an exception!
-                    throw new RuntimeException(
-                        'Cannot find template for recommendation class: ' .
-                        get_class($recommend)
-                    );
-                }
-            }
-        }
+        $context = ['recommend' => $recommend];
+        return $this->renderClassTemplate($template, $className, $context);
     }
 }
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Record.php b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
index cea7cb35a3a..036afbd262a 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Record.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
@@ -28,8 +28,6 @@
 namespace VuFind\View\Helper\Root;
 
 use VuFind\Cover\Router as CoverRouter;
-use Zend\View\Exception\RuntimeException;
-use Zend\View\Helper\AbstractHelper;
 
 /**
  * Record driver view helper
@@ -40,7 +38,7 @@ use Zend\View\Helper\AbstractHelper;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class Record extends AbstractHelper
+class Record extends AbstractClassBasedTemplateRenderer
 {
     /**
      * Context view helper
@@ -104,41 +102,11 @@ class Record extends AbstractHelper
      */
     public function renderTemplate($name, $context = null)
     {
-        // Set default context if none provided:
-        if (null === $context) {
-            $context = ['driver' => $this->driver];
-        }
-
-        // Set up the needed context in the view:
-        $oldContext = $this->contextHelper->apply($context);
-
-        // Get the current record driver's class name, then start a loop
-        // in case we need to use a parent class' name to find the appropriate
-        // template.
+        $template = 'RecordDriver/%s/' . $name;
         $className = get_class($this->driver);
-        $resolver = $this->view->resolver();
-        while (true) {
-            // Guess the template name for the current class:
-            $classParts = explode('\\', $className);
-            $template = 'RecordDriver/' . array_pop($classParts) . '/' . $name;
-            if ($resolver->resolve($template)) {
-                // Try to render the template....
-                $html = $this->view->render($template);
-                $this->contextHelper->restore($oldContext);
-                return $html;
-            } else {
-                // If the template doesn't exist, let's see if we can inherit a
-                // template from a parent class:
-                $className = get_parent_class($className);
-                if (empty($className)) {
-                    // No more parent classes left to try?  Throw an exception!
-                    throw new RuntimeException(
-                        'Cannot find ' . $name . ' template for record driver: ' .
-                        get_class($this->driver)
-                    );
-                }
-            }
-        }
+        return $this->renderClassTemplate(
+            $template, $className, $context ?? ['driver' => $this->driver]
+        );
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Related.php b/module/VuFind/src/VuFind/View/Helper/Root/Related.php
index 184d8457e91..a1c8203b6fe 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Related.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Related.php
@@ -27,9 +27,6 @@
  */
 namespace VuFind\View\Helper\Root;
 
-use Zend\View\Exception\RuntimeException;
-use Zend\View\Helper\AbstractHelper;
-
 /**
  * Related records view helper
  *
@@ -39,7 +36,7 @@ use Zend\View\Helper\AbstractHelper;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class Related extends AbstractHelper
+class Related extends AbstractClassBasedTemplateRenderer
 {
     /**
      * Plugin manager for related record modules.
@@ -81,38 +78,9 @@ class Related extends AbstractHelper
      */
     public function render($related)
     {
-        // Set up the rendering context:
-        $contextHelper = $this->getView()->plugin('context');
-        $oldContext = $contextHelper($this->getView())->apply(
-            ['related' => $related]
-        );
-
-        // Get the current related item module's class name, then start a loop
-        // in case we need to use a parent class' name to find the appropriate
-        // template.
+        $template = 'Related/%s.phtml';
         $className = get_class($related);
-        $resolver = $this->getView()->resolver();
-        while (true) {
-            // Guess the template name for the current class:
-            $classParts = explode('\\', $className);
-            $template = 'Related/' . array_pop($classParts) . '.phtml';
-            // Try to resolve the template....
-            if ($resolver->resolve($template)) {
-                $html = $this->getView()->render($template);
-                $contextHelper($this->getView())->restore($oldContext);
-                return $html;
-            } else {
-                // If the template doesn't exist, let's see if we can inherit a
-                // template from a parent class:
-                $className = get_parent_class($className);
-                if (empty($className)) {
-                    // No more parent classes left to try?  Throw an exception!
-                    throw new RuntimeException(
-                        'Cannot find template for related items class: ' .
-                        get_class($related)
-                    );
-                }
-            }
-        }
+        $context = ['related' => $related];
+        return $this->renderClassTemplate($template, $className, $context);
     }
 }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
index b3f0715824a..7aa7e774ddb 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
@@ -47,7 +47,7 @@ class RecordTest extends \PHPUnit\Framework\TestCase
      * @return void
      *
      * @expectedException        Zend\View\Exception\RuntimeException
-     * @expectedExceptionMessage Cannot find core.phtml template for record driver: VuFind\RecordDriver\SolrMarc
+     * @expectedExceptionMessage Cannot find RecordDriver/AbstractBase/core.phtml template for class: VuFind\RecordDriver\SolrMarc
      */
     public function testMissingTemplate()
     {
@@ -71,7 +71,9 @@ class RecordTest extends \PHPUnit\Framework\TestCase
         $record->getView()->resolver()->expects($this->at(0))->method('resolve')
             ->with($this->equalTo('RecordDriver/SolrMarc/collection-record.phtml'))
             ->will($this->returnValue(false));
-        $this->setSuccessTemplate($record, 'RecordDriver/SolrDefault/collection-record.phtml', 'success', 1);
+        $this->setSuccessTemplate(
+            $record, 'RecordDriver/SolrDefault/collection-record.phtml', 'success', 1, 3
+        );
         $this->assertEquals('success', $record->getCollectionBriefRecord());
     }
 
@@ -183,7 +185,9 @@ class RecordTest extends \PHPUnit\Framework\TestCase
         // that one fail so we can load the parent class' template instead:
         $record->getView()->resolver()->expects($this->at(0))->method('resolve')
             ->will($this->returnValue(false));
-        $this->setSuccessTemplate($record, 'RecordDriver/AbstractBase/list-entry.phtml', 'success', 1, 1);
+        $this->setSuccessTemplate(
+            $record, 'RecordDriver/AbstractBase/list-entry.phtml', 'success', 1, 3
+        );
         $this->assertEquals('success', $record->getListEntry(null, $user));
     }
 
@@ -492,21 +496,18 @@ class RecordTest extends \PHPUnit\Framework\TestCase
         if (null === $context) {
             $context = $this->getMockContext();
         }
-        $view = $this->createMock('Zend\View\Renderer\PhpRenderer');
-        if ($url) {
-            $url = $this->getMockUrl($url);
-        }
-        if (false !== $serverurl) {
-            $serverurl = $this->getMockServerUrl();
-        }
+        $view = $this->getMockBuilder('Zend\View\Renderer\PhpRenderer')
+            ->disableOriginalConstructor()
+            ->setMethods(['render', 'plugin', 'resolver'])
+            ->getMock();
         $pluginCallback = function ($helper) use ($context, $url, $serverurl) {
             switch ($helper) {
             case 'context':
                 return $context;
             case 'serverurl':
-                return $serverurl;
+                return $serverurl ? $this->getMockServerUrl() : false;
             case 'url':
-                return $url;
+                return $url ? $this->getMockUrl($url) : $url;
             case 'searchTabs':
                 return $this->getMockSearchTabs();
             default:
@@ -626,8 +627,9 @@ class RecordTest extends \PHPUnit\Framework\TestCase
      *
      * @return void
      */
-    protected function setSuccessTemplate($record, $tpl, $response = 'success', $resolveAt = 0, $renderAt = 1)
-    {
+    protected function setSuccessTemplate($record, $tpl, $response = 'success',
+        $resolveAt = 0, $renderAt = 2
+    ) {
         $expectResolve = $resolveAt === '*' ? $this->any() : $this->at($resolveAt);
         $record->getView()->resolver()->expects($expectResolve)->method('resolve')
             ->with($this->equalTo($tpl))
-- 
GitLab