[FEATURE] Template Path fallback for Fluid Standalone View 13/32613/17
authorAnja Leichsenring <aleichsenring@ab-softlab.de>
Sat, 6 Sep 2014 20:46:49 +0000 (22:46 +0200)
committerWouter Wolters <typo3@wouterwolters.nl>
Tue, 7 Oct 2014 21:53:15 +0000 (23:53 +0200)
TemplateView received the feature of template path fallback
configuration with issue #39868.
The same is missing for Standalone view and will be introduced
with this patch.

Change-Id: I1e854a356a4c7e9cfff773c8f56fe4a437f77920
Resolves: #61361
Related: #39868
Releases: master
Reviewed-on: http://review.typo3.org/32613
Reviewed-by: Markus Klein <klein.t3@reelworx.at>
Tested-by: Markus Klein <klein.t3@reelworx.at>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
typo3/sysext/core/Documentation/Changelog/master/Feature-61361-FallbackTemplatePathsForFluidStandaloneView.rst [new file with mode: 0644]
typo3/sysext/fluid/Classes/View/StandaloneView.php
typo3/sysext/fluid/Tests/Unit/View/StandaloneViewTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-61361-FallbackTemplatePathsForFluidStandaloneView.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-61361-FallbackTemplatePathsForFluidStandaloneView.rst
new file mode 100644 (file)
index 0000000..916d534
--- /dev/null
@@ -0,0 +1,50 @@
+=================================================================
+Feature: #61361 - Template Path Fallback for Fluid StandaloneView
+=================================================================
+
+Description
+===========
+
+Earlier in the development of Fluid, a template fallback was introduced
+in the TemplateView, providing the possibility to pass a set of possible
+file locations to the View Configuration, where Templates, Layouts and Partials
+can be found.
+
+The same functionality is now in the StandaloneView. It is possible to
+let the system look up the fitting paths for Partials and Layouts. It is
+in the nature of the StandaloneView to get a specific template file set, so
+for Templates there is no lookup requirement.
+
+As a developer or integrator, you can configure your View as follows:
+
+::
+$view = $this->objectManager->get('TYPO3\\CMS\\Fluid\\View\\StandaloneView');
+$view->setFormat('html');
+$view->setTemplatePathAndFileName(ExtensionManagementUtility::extPath('myExt') . 'Resources/Private/Templates/Email.html');
+$view->setLayoutRootPaths(array(
+  'default' => ExtensionManagementUtility::extPath('myExt') . 'Resources/Private/Layouts',
+  'specific' => ExtensionManagementUtility::extPath('myTemplateExt') . 'Resources/Private/Layouts/MyExt',
+));
+$view->setPartialRootPaths(array(
+  'default' => ExtensionManagementUtility::extPath('myExt') . 'Resources/Private/Partials',
+  'specific' => ExtensionManagementUtility::extPath('myTemplateExt') . 'Resources/Private/Layouts/MyExt',
+  'evenMoreSpecific' => 'fileAdmin/templates/myExt/Partials',
+));
+..
+
+With this, the View will first look up the requested layout file in the path with the key
+_specific_, and in case there is no such file, it will fall back to _default_. For the partials the
+sequence would be _evenMoreSpecific_, then _specific_, then fall back to _default_.
+
+You are free in the naming
+of the keys. The paths are searched from bottom to top.
+In case you choose for numeric array keys, the array is ordered first, then reversed for the lookup, so
+the highest index is accessed first.
+
+Impact
+======
+
+In order to change the skin of an extension output, provided by the Fluid StandaloneView, you are no longer required to
+copy the whole Resources folder into fileadmin or to some specific location, but you can pick only the files you want
+to change. Those need to be organized in folders, which are then configured for the view. The system will fall through
+all the provided locations, taking the first fitting file it finds.
\ No newline at end of file
index 0d8db74..429779d 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 namespace TYPO3\CMS\Fluid\View;
 
-/*                                                                        *
+/**                                                                       *
  * This script is backported from the TYPO3 Flow package "TYPO3.Fluid".   *
  *                                                                        *
  * It is free software; you can redistribute it and/or modify it under    *
@@ -20,13 +20,18 @@ namespace TYPO3\CMS\Fluid\View;
  *                                                                        *
  * The TYPO3 project - inspiring people to share!                         *
  *                                                                        */
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Utility\ArrayUtility;
+use TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+
 /**
  * A standalone template view.
  * Should be used as view if you want to use Fluid without Extbase extensions
  *
  * @api
  */
-class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
+class StandaloneView extends AbstractTemplateView {
 
        /**
         * Source code of the Fluid template
@@ -43,51 +48,52 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
        protected $templatePathAndFilename = NULL;
 
        /**
-        * absolute root path of the folder that contains Fluid layouts
+        * Path(s) to the partial root
         *
-        * @var string
+        * @var array
         */
-       protected $layoutRootPath = NULL;
+       protected $partialRootPaths = NULL;
 
        /**
-        * absolute root path of the folder that contains Fluid partials
+        * Path(s) to the layout root
         *
-        * @var string
-        */
-       protected $partialRootPath = NULL;
-
-       /**
-        * @var \TYPO3\CMS\Fluid\Core\Compiler\TemplateCompiler
+        * @var array
         */
-       protected $templateCompiler;
+       protected $layoutRootPaths = NULL;
 
        /**
         * Constructor
         *
-        * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObject The current cObject. If NULL a new instance will be created
+        * @param ContentObjectRenderer $contentObject The current cObject. If NULL a new instance will be created
+        * @throws \InvalidArgumentException
+        * @throws \UnexpectedValueException
         */
-       public function __construct(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObject = NULL) {
-               $this->objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager');
+       public function __construct(ContentObjectRenderer $contentObject = NULL) {
+               $this->objectManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager');
+               /** @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager */
                $configurationManager = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Configuration\\ConfigurationManagerInterface');
                if ($contentObject === NULL) {
-                       /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObject */
-                       $contentObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\ContentObject\\ContentObjectRenderer');
+                       /** @var ContentObjectRenderer $contentObject */
+                       $contentObject = GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\ContentObject\\ContentObjectRenderer');
                }
                $configurationManager->setContentObject($contentObject);
                $this->templateParser = $this->objectManager->get('TYPO3\\CMS\\Fluid\\Core\\Parser\\TemplateParser');
                $this->setRenderingContext($this->objectManager->get('TYPO3\\CMS\\Fluid\\Core\\Rendering\\RenderingContext'));
+               /** @var \TYPO3\CMS\Extbase\Mvc\Web\Request $request */
                $request = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Mvc\\Web\\Request');
-               $request->setRequestURI(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
-               $request->setBaseURI(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'));
+               $request->setRequestURI(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
+               $request->setBaseURI(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'));
+               /** @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder $uriBuilder */
                $uriBuilder = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Mvc\\Web\\Routing\\UriBuilder');
                $uriBuilder->setRequest($request);
+               /** @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext */
                $controllerContext = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ControllerContext');
                $controllerContext->setRequest($request);
                $controllerContext->setUriBuilder($uriBuilder);
                $this->setControllerContext($controllerContext);
                $this->templateCompiler = $this->objectManager->get('TYPO3\\CMS\\Fluid\\Core\\Compiler\\TemplateCompiler');
                // singleton
-               $this->templateCompiler->setTemplateCache(\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Cache\\CacheManager')->getCache('fluid_template'));
+               $this->templateCompiler->setTemplateCache(GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Cache\\CacheManager')->getCache('fluid_template'));
        }
 
        /**
@@ -154,57 +160,117 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
        }
 
        /**
-        * Sets the absolute path to the folder that contains Fluid layout files
+        * Set the root path to the layouts.
         *
-        * @param string $layoutRootPath Fluid layout root path
+        * @param string $layoutRootPath Root path to the layouts.
         * @return void
         * @api
+        * @see setLayoutRootPaths()
+        * @deprecated since Fluid 7; Use setLayoutRootPaths() instead
         */
        public function setLayoutRootPath($layoutRootPath) {
-               $this->layoutRootPath = $layoutRootPath;
+               GeneralUtility::logDeprecatedFunction();
+               $this->setLayoutRootPaths(array($layoutRootPath));
        }
 
        /**
-        * Returns the absolute path to the folder that contains Fluid layout files
+        * Set the root path(s) to the layouts.
         *
-        * @return string Fluid layout root path
+        * @param array $layoutRootPaths Root path to the layouts
+        * @return void
         * @api
         */
+       public function setLayoutRootPaths(array $layoutRootPaths) {
+               $this->layoutRootPaths = $layoutRootPaths;
+       }
+
+       /**
+        * Returns the first found entry in $this->layoutRootPaths.
+        * Don't use, this might not be the desired result.
+        *
+        * @throws InvalidTemplateResourceException
+        * @return string Path to layout root directory
+        * @deprecated since Fluid 7; Use getLayoutRootPaths() instead
+        */
        public function getLayoutRootPath() {
-               if ($this->layoutRootPath === NULL && $this->templatePathAndFilename === NULL) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('No layout root path has been specified. Use setLayoutRootPath().', 1288091419);
+               GeneralUtility::logDeprecatedFunction();
+               $layoutRootPaths = $this->getLayoutRootPaths();
+               return array_shift($layoutRootPaths);
+       }
+
+       /**
+        * Resolves the layout root to be used inside other paths.
+        *
+        * @return string Fluid layout root path
+        * @throws InvalidTemplateResourceException
+        * @api
+        */
+       public function getLayoutRootPaths() {
+               if ($this->layoutRootPaths === NULL && $this->templatePathAndFilename === NULL) {
+                       throw new InvalidTemplateResourceException('No layout root path has been specified. Use setLayoutRootPaths().', 1288091419);
                }
-               if ($this->layoutRootPath === NULL) {
-                       $this->layoutRootPath = dirname($this->templatePathAndFilename) . '/Layouts';
+               if ($this->layoutRootPaths === NULL) {
+                       $this->layoutRootPaths = array(dirname($this->templatePathAndFilename) . '/Layouts');
                }
-               return $this->layoutRootPath;
+               return $this->layoutRootPaths;
        }
 
        /**
-        * Sets the absolute path to the folder that contains Fluid partial files.
+        * Set the root path to the partials.
+        * If set, overrides the one determined from $this->partialRootPathPattern
         *
-        * @param string $partialRootPath Fluid partial root path
+        * @param string $partialRootPath Root path to the partials. If set, overrides the one determined from $this->partialRootPathPattern
         * @return void
         * @api
+        * @see setPartialRootPaths()
+        * @deprecated since Fluid 7; Use setPartialRootPaths() instead
         */
        public function setPartialRootPath($partialRootPath) {
-               $this->partialRootPath = $partialRootPath;
+               GeneralUtility::logDeprecatedFunction();
+               $this->setPartialRootPaths(array($partialRootPath));
+       }
+
+       /**
+        * Returns the first found entry in $this->partialRootPaths
+        * Don't use, this might not be the desired result.
+        *
+        * @throws InvalidTemplateResourceException
+        * @return string Path to partial root directory
+        * @deprecated since Fluid 7; Use getPartialRootPaths() instead
+        */
+       public function getPartialRootPath() {
+               GeneralUtility::logDeprecatedFunction();
+               $partialRootPaths = $this->getPartialRootPaths();
+               return array_shift($partialRootPaths);
+       }
+
+       /**
+        * Set the root path(s) to the partials.
+        * If set, overrides the one determined from $this->partialRootPathPattern
+        *
+        * @param array $partialRootPaths Root paths to the partials. If set, overrides the one determined from $this->partialRootPathPattern
+        * @return void
+        * @api
+        */
+       public function setPartialRootPaths(array $partialRootPaths) {
+               $this->partialRootPaths = $partialRootPaths;
        }
 
        /**
         * Returns the absolute path to the folder that contains Fluid partial files
         *
         * @return string Fluid partial root path
+        * @throws InvalidTemplateResourceException
         * @api
         */
-       public function getPartialRootPath() {
-               if ($this->partialRootPath === NULL && $this->templatePathAndFilename === NULL) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('No partial root path has been specified. Use setPartialRootPath().', 1288094511);
+       public function getPartialRootPaths() {
+               if ($this->partialRootPaths === NULL && $this->templatePathAndFilename === NULL) {
+                       throw new InvalidTemplateResourceException('No partial root path has been specified. Use setPartialRootPaths().', 1288094511);
                }
-               if ($this->partialRootPath === NULL) {
-                       $this->partialRootPath = dirname($this->templatePathAndFilename) . '/Partials';
+               if ($this->partialRootPaths === NULL) {
+                       $this->partialRootPaths = array(dirname($this->templatePathAndFilename) . '/Partials');
                }
-               return $this->partialRootPath;
+               return $this->partialRootPaths;
        }
 
        /**
@@ -217,7 +283,7 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
                try {
                        $this->getTemplateSource();
                        return TRUE;
-               } catch (\TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException $e) {
+               } catch (InvalidTemplateResourceException $e) {
                        return FALSE;
                }
        }
@@ -228,6 +294,7 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $actionName Name of the action. This argument is not used in this view!
         * @return string template identifier
+        * @throws InvalidTemplateResourceException
         */
        protected function getTemplateIdentifier($actionName = NULL) {
                if ($this->templateSource === NULL) {
@@ -249,15 +316,15 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $actionName Name of the action. This argument is not used in this view!
         * @return string Fluid template source
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws InvalidTemplateResourceException
         */
        protected function getTemplateSource($actionName = NULL) {
                if ($this->templateSource === NULL && $this->templatePathAndFilename === NULL) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('No template has been specified. Use either setTemplateSource() or setTemplatePathAndFilename().', 1288085266);
+                       throw new InvalidTemplateResourceException('No template has been specified. Use either setTemplateSource() or setTemplatePathAndFilename().', 1288085266);
                }
                if ($this->templateSource === NULL) {
                        if (!is_file($this->templatePathAndFilename)) {
-                               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Template could not be found at "' . $this->templatePathAndFilename . '".', 1288087061);
+                               throw new InvalidTemplateResourceException('Template could not be found at "' . $this->templatePathAndFilename . '".', 1288087061);
                        }
                        $this->templateSource = file_get_contents($this->templatePathAndFilename);
                }
@@ -270,6 +337,7 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $layoutName The name of the layout
         * @return string layout identifier
+        * @throws InvalidTemplateResourceException
         */
        protected function getLayoutIdentifier($layoutName = 'Default') {
                $layoutPathAndFilename = $this->getLayoutPathAndFilename($layoutName);
@@ -279,47 +347,59 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        /**
         * Resolves the path and file name of the layout file, based on
-        * $this->getLayoutRootPath() and request format and returns the file contents
+        * $this->getLayoutRootPaths() and request format and returns the file contents
         *
-        * @param string $layoutName Name of the layout to use. If none given, use "Default
+        * @param string $layoutName Name of the layout to use. If none given, use "Default"
         * @return string contents of the layout file if it was found
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws InvalidTemplateResourceException
         */
        protected function getLayoutSource($layoutName = 'Default') {
                $layoutPathAndFilename = $this->getLayoutPathAndFilename($layoutName);
                $layoutSource = file_get_contents($layoutPathAndFilename);
                if ($layoutSource === FALSE) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('"' . $layoutPathAndFilename . '" is not a valid template resource URI.', 1312215888);
+                       throw new InvalidTemplateResourceException('"' . $layoutPathAndFilename . '" is not a valid template resource URI.', 1312215888);
                }
                return $layoutSource;
        }
 
        /**
         * Resolve the path and file name of the layout file, based on
-        * $this->getLayoutRootPath() and request format
+        * $this->getLayoutRootPaths() and request format
         *
         * In case a layout has already been set with setLayoutPathAndFilename(),
         * this method returns that path, otherwise a path and filename will be
         * resolved using the layoutPathAndFilenamePattern.
         *
-        * @param string $layoutName Name of the layout to use. If none given, use "Default
+        * @param string $layoutName Name of the layout to use. If none given, use "Default"
         * @return string Path and filename of layout files
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws InvalidTemplateResourceException
         */
        protected function getLayoutPathAndFilename($layoutName = 'Default') {
-               $layoutRootPath = $this->getLayoutRootPath();
-               if (!is_dir($layoutRootPath)) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Layout root path "' . $layoutRootPath . '" does not exist.', 1288092521);
-               }
+               $layoutName = ucfirst($layoutName);
                $possibleLayoutPaths = array();
-               $possibleLayoutPaths[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath($layoutRootPath . '/' . $layoutName . '.' . $this->getRequest()->getFormat());
-               $possibleLayoutPaths[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath($layoutRootPath . '/' . $layoutName);
+               $paths = ArrayUtility::sortArrayWithIntegerKeys($this->getLayoutRootPaths());
+               $paths = array_reverse($paths, TRUE);
+               foreach ($paths as $layoutRootPath) {
+                       $possibleLayoutPaths[] = GeneralUtility::fixWindowsFilePath($layoutRootPath . '/' . $layoutName . '.' . $this->getRequest()->getFormat());
+                       $possibleLayoutPaths[] = GeneralUtility::fixWindowsFilePath($layoutRootPath . '/' . $layoutName);
+               }
                foreach ($possibleLayoutPaths as $layoutPathAndFilename) {
-                       if (is_file($layoutPathAndFilename)) {
+                       if ($this->testFileExistence($layoutPathAndFilename)) {
                                return $layoutPathAndFilename;
                        }
                }
-               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Could not load layout file. Tried following paths: "' . implode('", "', $possibleLayoutPaths) . '".', 1288092555);
+
+               throw new InvalidTemplateResourceException('Could not load layout file. Tried following paths: "' . implode('", "', $possibleLayoutPaths) . '".', 1288092555);
+       }
+
+       /**
+        * Wrapper method for is_file function for testing reasons
+        *
+        * @param string $filePath
+        * @return bool
+        */
+       protected function testFileExistence($filePath) {
+               return is_file($filePath);
        }
 
        /**
@@ -328,6 +408,7 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $partialName The name of the partial
         * @return string partial identifier
+        * @throws InvalidTemplateResourceException
         */
        protected function getPartialIdentifier($partialName) {
                $partialPathAndFilename = $this->getPartialPathAndFilename($partialName);
@@ -341,38 +422,39 @@ class StandaloneView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $partialName The name of the partial
         * @return string contents of the layout file if it was found
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws InvalidTemplateResourceException
         */
        protected function getPartialSource($partialName) {
                $partialPathAndFilename = $this->getPartialPathAndFilename($partialName);
                $partialSource = file_get_contents($partialPathAndFilename);
                if ($partialSource === FALSE) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('"' . $partialPathAndFilename . '" is not a valid template resource URI.', 1257246932);
+                       throw new InvalidTemplateResourceException('"' . $partialPathAndFilename . '" is not a valid template resource URI.', 1257246932);
                }
                return $partialSource;
        }
 
        /**
-        * Resolve the partial path and filename based on $this->getPartialRootPath() and request format
+        * Resolve the partial path and filename based on $this->getPartialRootPaths() and request format
         *
         * @param string $partialName The name of the partial
-        * @return string the full path which should be used. The path definitely exists.
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @return string The full path which should be used. The path definitely exists.
+        * @throws InvalidTemplateResourceException
         */
        protected function getPartialPathAndFilename($partialName) {
-               $partialRootPath = $this->getPartialRootPath();
-               if (!is_dir($partialRootPath)) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Partial root path "' . $partialRootPath . '" does not exist.', 1288094648);
-               }
+               $partialName = ucfirst($partialName);
+               $paths = ArrayUtility::sortArrayWithIntegerKeys($this->getPartialRootPaths());
+               $paths = array_reverse($paths, TRUE);
                $possiblePartialPaths = array();
-               $possiblePartialPaths[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath($partialRootPath . '/' . $partialName . '.' . $this->getRequest()->getFormat());
-               $possiblePartialPaths[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath($partialRootPath . '/' . $partialName);
+               foreach ($paths as $partialRootPath) {
+                       $possiblePartialPaths[] = GeneralUtility::fixWindowsFilePath($partialRootPath . '/' . $partialName . '.' . $this->getRequest()->getFormat());
+                       $possiblePartialPaths[] = GeneralUtility::fixWindowsFilePath($partialRootPath . '/' . $partialName);
+               }
                foreach ($possiblePartialPaths as $partialPathAndFilename) {
-                       if (is_file($partialPathAndFilename)) {
+                       if ($this->testFileExistence($partialPathAndFilename)) {
                                return $partialPathAndFilename;
                        }
                }
-               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Could not load partial file. Tried following paths: "' . implode('", "', $possiblePartialPaths) . '".', 1288092556);
+               throw new InvalidTemplateResourceException('Could not load partial file. Tried following paths: "' . implode('", "', $possiblePartialPaths) . '".', 1288092556);
        }
 
        /**
index f33f548..7eb9a24 100644 (file)
@@ -32,7 +32,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
        protected $singletonInstances = array();
 
        /**
-        * @var \TYPO3\CMS\Fluid\View\StandaloneView
+        * @var \TYPO3\CMS\Fluid\View\StandaloneView|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface
         */
        protected $view;
 
@@ -98,7 +98,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
         */
        public function setUp() {
                $this->singletonInstances = \TYPO3\CMS\Core\Utility\GeneralUtility::getSingletonInstances();
-               $this->view = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\StandaloneView', array('dummy'), array(), '', FALSE);
+               $this->view = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\StandaloneView', array('testFileExistence'), array(), '', FALSE);
                $this->mockTemplateParser = $this->getMock('TYPO3\\CMS\\Fluid\\Core\\Parser\\TemplateParser');
                $this->mockParsedTemplate = $this->getMock('TYPO3\\CMS\\Fluid\\Core\\Parser\\ParsedTemplateInterface');
                $this->mockTemplateParser->expects($this->any())->method('parse')->will($this->returnValue($this->mockParsedTemplate));
@@ -276,6 +276,14 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
 
        /**
         * @test
+        * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        */
+       public function getLayoutRootPathsThrowsExceptionIfLayoutRootPathAndTemplatePathAreNotSpecified() {
+               $this->view->getLayoutRootPaths();
+       }
+
+       /**
+        * @test
         */
        public function getLayoutRootPathReturnsSpecifiedLayoutRootPathByDefault() {
                $templatePathAndFilename = 'some/template/RootPath/SomeTemplate.html';
@@ -289,6 +297,20 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
        /**
         * @test
         */
+       public function getLayoutRootPathsReturnsSpecifiedLayoutRootPathByDefault() {
+               $templatePathAndFilename = 'some/template/RootPath/SomeTemplate.html';
+               $layoutRootPaths = array(
+                       'some/layout/RootPath'
+               );
+               $this->view->setTemplatePathAndFilename($templatePathAndFilename);
+               $this->view->setLayoutRootPaths($layoutRootPaths);
+               $actualResult = $this->view->getLayoutRootPaths();
+               $this->assertEquals($layoutRootPaths, $actualResult);
+       }
+
+       /**
+        * @test
+        */
        public function getLayoutRootPathReturnsDefaultPathIfNoLayoutRootPathIsSpecified() {
                $templatePathAndFilename = 'some/template/RootPath/SomeTemplate.html';
                $this->view->setTemplatePathAndFilename($templatePathAndFilename);
@@ -299,6 +321,17 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
 
        /**
         * @test
+        */
+       public function getLayoutRootPathsReturnsDefaultPathIfNoLayoutRootPathIsSpecified() {
+               $templatePathAndFilename = 'some/template/RootPath/SomeTemplate.html';
+               $this->view->setTemplatePathAndFilename($templatePathAndFilename);
+               $expectedResult = array('some/template/RootPath/Layouts');
+               $actualResult = $this->view->getLayoutRootPaths();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
         * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
         */
        public function getLayoutSourceThrowsExceptionIfLayoutRootPathDoesNotExist() {
@@ -310,9 +343,18 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
         * @test
         * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
         */
+       public function getLayoutSourceThrowsExceptionIfLayoutRootPathsDoesNotExist() {
+               $this->view->setLayoutRootPaths(array('some/non/existing/Path'));
+               $this->view->_call('getLayoutSource');
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        */
        public function getLayoutSourceThrowsExceptionIfLayoutFileDoesNotExist() {
                $layoutRootPath = __DIR__ . '/Fixtures';
-               $this->view->setLayoutRootPath($layoutRootPath);
+               $this->view->setLayoutRootPaths(array($layoutRootPath));
                $this->view->_call('getLayoutSource', 'NonExistingLayout');
        }
 
@@ -323,6 +365,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $layoutRootPath = __DIR__ . '/Fixtures';
                $this->view->setLayoutRootPath($layoutRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->once())->method('testFileExistence')->with($layoutRootPath . '/LayoutFixture.html')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($layoutRootPath . '/LayoutFixture.html');
                $actualResult = $this->view->_call('getLayoutSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
@@ -335,6 +378,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $layoutRootPath = __DIR__ . '/Fixtures';
                $this->view->setLayoutRootPath($layoutRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('xml'));
+               $this->view->expects($this->once())->method('testFileExistence')->with($layoutRootPath . '/LayoutFixture.xml')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($layoutRootPath . '/LayoutFixture.xml');
                $actualResult = $this->view->_call('getLayoutSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
@@ -347,6 +391,8 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $layoutRootPath = __DIR__ . '/Fixtures';
                $this->view->setLayoutRootPath($layoutRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('foo'));
+               $this->view->expects($this->at(0))->method('testFileExistence')->with($layoutRootPath . '/LayoutFixture.foo')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(1))->method('testFileExistence')->with($layoutRootPath . '/LayoutFixture')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($layoutRootPath . '/LayoutFixture');
                $actualResult = $this->view->_call('getLayoutSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
@@ -409,6 +455,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $partialRootPath = __DIR__ . '/Fixtures';
                $this->view->setPartialRootPath($partialRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->once())->method('testFileExistence')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($partialRootPath . '/LayoutFixture.html');
                $actualResult = $this->view->_call('getPartialSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
@@ -421,6 +468,7 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $partialRootPath = __DIR__ . '/Fixtures';
                $this->view->setPartialRootPath($partialRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('xml'));
+               $this->view->expects($this->once())->method('testFileExistence')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($partialRootPath . '/LayoutFixture.xml');
                $actualResult = $this->view->_call('getPartialSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
@@ -433,8 +481,247 @@ class StandaloneViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $partialRootPath = __DIR__ . '/Fixtures';
                $this->view->setPartialRootPath($partialRootPath);
                $this->mockRequest->expects($this->once())->method('getFormat')->will($this->returnValue('foo'));
+               $this->view->expects($this->at(1))->method('testFileExistence')->will($this->returnValue(TRUE));
                $expectedResult = file_get_contents($partialRootPath . '/LayoutFixture');
                $actualResult = $this->view->_call('getPartialSource', 'LayoutFixture');
                $this->assertEquals($expectedResult, $actualResult);
        }
+
+       /**
+        * @test
+        */
+       public function setPartialsRootPathOverrulesSetTemplateRootPaths() {
+               $this->view->setPartialRootPath('/foo/bar');
+               $this->view->setPartialRootPaths(array('/overruled/path'));
+               $expected = array('/overruled/path');
+               $actual = $this->view->_call('getPartialRootPaths');
+               $this->assertEquals($expected, $actual, 'A set template root path was not returned correctly.');
+       }
+
+       /**
+        * @test
+        */
+       public function setLayoutsRootPathOverrulesSetTemplateRootPaths() {
+               $this->view->setLayoutRootPath('/foo/bar');
+               $this->view->setLayoutRootPaths(array('/overruled/path'));
+               $expected = array('/overruled/path');
+               $actual = $this->view->_call('getLayoutRootPaths');
+               $this->assertEquals($expected, $actual, 'A set template root path was not returned correctly.');
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameResolvesTheSpecificFile() {
+               $this->view->setLayoutRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Layouts',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->once())->method('testFileExistence')->with('specific/Layouts/Default.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('specific/Layouts/Default.html', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameResolvesTheDefaultFile() {
+               $this->view->setLayoutRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Layouts',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('some/Default/Directory/Default.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Default.html', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameResolvesTheSpecificFileWithNumericIndices() {
+               $this->view->setLayoutRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Layouts',
+                       '17' => 'specific/Layouts',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Layouts/Default.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('specific/Layouts/Default.html', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameResolvesTheDefaultFileWithNumericIndices() {
+               $this->view->setLayoutRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Layouts',
+                       '17' => 'specific/Layouts',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Default.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Default.html', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @expectedExceptionCode 1288092555
+        */
+       public function getLayoutPathAndFilenameThrowsExceptionIfNoFileWasFound() {
+               $this->view->setLayoutRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Layouts',
+                       '17' => 'specific/Layouts',
+               ));
+               $this->view->expects($this->any())->method('testFileExistence')->will($this->returnValue(FALSE));
+               $this->view->_call('getLayoutPathAndFilename');
+
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameResolvesTheSpecificFile() {
+               $this->view->setPartialRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->once())->method('testFileExistence')->with('specific/Partials/Partial.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('specific/Partials/Partial.html', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameResolvesTheDefaultFile() {
+               $this->view->setPartialRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('some/Default/Directory/Partial.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Partial.html', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameResolvesTheSpecificFileWithNumericIndices() {
+               $this->view->setPartialRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Partials',
+                       '17' => 'specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Partials/Partial.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('specific/Partials/Partial.html', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameResolvesTheDefaultFileWithNumericIndices() {
+               $this->view->setPartialRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Partials',
+                       '17' => 'specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Partial.html')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Partial.html', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @expectedExceptionCode 1288092556
+        */
+       public function getPartialPathAndFilenameThrowsExceptionIfNoFileWasFound() {
+               $this->view->setPartialRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Partials',
+                       '17' => 'specific/Partials',
+               ));
+               $this->view->expects($this->any())->method('testFileExistence')->will($this->returnValue(FALSE));
+               $this->view->_call('getPartialPathAndFilename', 'Partial');
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameWalksNumericalIndicesInDescendingOrder() {
+               $this->view->setPartialRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Partials',
+                       '17' => 'specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(0))->method('testFileExistence')->with('evenMore/Specific/Partials/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(1))->method('testFileExistence')->with('evenMore/Specific/Partials/Partial')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Partials/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(3))->method('testFileExistence')->with('specific/Partials/Partial')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(5))->method('testFileExistence')->with('some/Default/Directory/Partial')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Partial', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameWalksNumericalIndicesInDescendingOrder() {
+               $this->view->setLayoutRootPaths(array(
+                       '10' => 'some/Default/Directory',
+                       '25' => 'evenMore/Specific/Layouts',
+                       '17' => 'specific/Layouts',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(0))->method('testFileExistence')->with('evenMore/Specific/Layouts/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(1))->method('testFileExistence')->with('evenMore/Specific/Layouts/Default')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Layouts/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(3))->method('testFileExistence')->with('specific/Layouts/Default')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(5))->method('testFileExistence')->with('some/Default/Directory/Default')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Default', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
+       /**
+        * @test
+        */
+       public function getPartialPathAndFilenameWalksStringKeysInReversedOrder() {
+               $this->view->setPartialRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Partials',
+                       'verySpecific' => 'evenMore/Specific/Partials',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(0))->method('testFileExistence')->with('evenMore/Specific/Partials/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(1))->method('testFileExistence')->with('evenMore/Specific/Partials/Partial')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Partials/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(3))->method('testFileExistence')->with('specific/Partials/Partial')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Partial.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(5))->method('testFileExistence')->with('some/Default/Directory/Partial')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Partial', $this->view->_call('getPartialPathAndFilename', 'Partial'));
+       }
+
+       /**
+        * @test
+        */
+       public function getLayoutPathAndFilenameWalksStringKeysInReversedOrder() {
+               $this->view->setLayoutRootPaths(array(
+                       'default' => 'some/Default/Directory',
+                       'specific' => 'specific/Layout',
+                       'verySpecific' => 'evenMore/Specific/Layout',
+               ));
+               $this->mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue('html'));
+               $this->view->expects($this->at(0))->method('testFileExistence')->with('evenMore/Specific/Layout/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(1))->method('testFileExistence')->with('evenMore/Specific/Layout/Default')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(2))->method('testFileExistence')->with('specific/Layout/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(3))->method('testFileExistence')->with('specific/Layout/Default')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(4))->method('testFileExistence')->with('some/Default/Directory/Default.html')->will($this->returnValue(FALSE));
+               $this->view->expects($this->at(5))->method('testFileExistence')->with('some/Default/Directory/Default')->will($this->returnValue(TRUE));
+               $this->assertEquals('some/Default/Directory/Default', $this->view->_call('getLayoutPathAndFilename'));
+       }
+
 }