[FEATURE] Auto-render and attach HeaderAssets and FooterAssets 80/51380/12
authorClaus Due <claus@namelesscoder.net>
Sun, 22 Jan 2017 12:31:30 +0000 (13:31 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Mon, 6 Feb 2017 19:58:28 +0000 (20:58 +0100)
This patch adds a new method on ActionController,
initializeAssetsForRequest($request) which comes
with a default implementation and allows overriding
in subclasses.

The default implementation of this new feature will
try to render sections HeaderAssets and FooterAssets
from the Fluid template that is resolved, assigning
the output (if not empty) to either header or footer,
by using the PageRenderer.

The feature only works for TemplateView and
subclasses thereof, since `renderSection` is not a
required method for ViewInterface implementations;
it only exists on the (MVC-centric) TemplateView.

Change-Id: Ia815410637982c077236e12a848bdd26ead22e69
Releases: master
Resolves: #79409
Reviewed-on: https://review.typo3.org/51380
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Documentation/Changelog/master/Feature-79409-AutorenderAssetSectionsInFluidTemplateWithController.rst [new file with mode: 0644]
typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
typo3/sysext/extbase/Tests/Unit/Mvc/Controller/ActionControllerTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79409-AutorenderAssetSectionsInFluidTemplateWithController.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79409-AutorenderAssetSectionsInFluidTemplateWithController.rst
new file mode 100644 (file)
index 0000000..b7465bb
--- /dev/null
@@ -0,0 +1,43 @@
+.. include:: ../../Includes.txt
+
+===============================================================================
+Feature: #73409 - Auto-render Assets sections in Fluid template with controller
+===============================================================================
+
+See :issue:`73409`
+
+
+Description
+===========
+
+ActionController has received a new method, `renderAssetsForRequest` which receives the `RequestInterface`
+Request currently being processed. The ActionController has a default implementation of this method which attempts
+to render two sections in the Fluid template that is associated with the controller action being called:
+
+* `<f:section name="HeaderAssets">` for assets intended for the `<head>` tag
+* `<f:section name="FooterAssets">` for assets intended for the end of the `<body>` tag
+
+Both sections are optional.
+
+When rendering, `{request}` is available as template variable in both sections, as is `{arguments}`, allowing you
+to make decisions based on various request/controller arguments. As usual, `{settings}` is also available.
+
+All content you write into these sections will be output in the respective location, meaning you must write the entire
+`<script>` or whichever tag you are writing, including all attributes. You can of course use various Fluid ViewHelpers
+to resolve extension asset paths.
+
+The feature only applies to ActionController (thus excluding CommandController) and will only attempt to render the
+section if the view is an instance of :php:`TYPO3Fluid\\Fluid\\View\\TemplateView` (thus including any View in TYPO3 which
+extends either TemplateView or AbstractTemplateView from TYPO3's Fluid adapter).
+
+Impact
+======
+
+* Fluid templates renderered through any ActionController using a TemplateView may now contain two new sections for
+  either `HeaderAssets` or `FooterAssets` depending on desired output. Content of these sections will be rendered
+  and assigned via PageRenderer to either header or footer.
+* ActionControllers can override the `renderAssetsForRequest` method to perform asset insertion using other means.
+  The method sits at a very opportune point right after the action method itself gets called, when the entire controller
+  is fully initialized with arguments etc. but no forwarding/rediction has happened in the controller action.
+
+.. index:: Fluid, Frontend
index dfc1942..07401ef 100644 (file)
@@ -15,11 +15,13 @@ namespace TYPO3\CMS\Extbase\Mvc\Controller;
  */
 
 use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
 use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
 use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest;
 use TYPO3\CMS\Extbase\Validation\Validator\AbstractCompositeValidator;
+use TYPO3Fluid\Fluid\View\TemplateView;
 
 /**
  * A multi action controller. This is by far the most common base class for Controllers.
@@ -173,6 +175,44 @@ class ActionController extends AbstractController
             $this->initializeView($this->view);
         }
         $this->callActionMethod();
+        $this->renderAssetsForRequest($request);
+    }
+
+    /**
+     * Method which initializes assets that should be attached to the response
+     * for the given $request, which contains parameters that an override can
+     * use to determine which assets to add via PageRenderer.
+     *
+     * This default implementation will attempt to render the sections "HeaderAssets"
+     * and "FooterAssets" from the template that is being rendered, inserting the
+     * rendered content into either page header or footer, as appropriate. Both
+     * sections are optional and can be used one or both in combination.
+     *
+     * You can add assets with this method without worrying about duplicates, if
+     * for example you do this in a plugin that gets used multiple time on a page.
+     *
+     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
+     * @return void
+     */
+    protected function renderAssetsForRequest($request)
+    {
+        if (!$this->view instanceof TemplateView) {
+            // Only TemplateView (from Fluid engine, so this includes all TYPO3 Views based
+            // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
+            // declared on ViewInterface - so we must assert a specific class. We silently skip
+            // asset processing if the View doesn't match, so we don't risk breaking custom Views.
+            return;
+        }
+        $pageRenderer = $this->objectManager->get(PageRenderer::class);
+        $variables = ['request' => $request, 'arguments' => $this->arguments];
+        $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
+        $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
+        if (!empty(trim($headerAssets))) {
+            $pageRenderer->addHeaderData($headerAssets);
+        }
+        if (!empty(trim($footerAssets))) {
+            $pageRenderer->addFooterData($footerAssets);
+        }
     }
 
     /**
index ac7ea2a..4e15508 100644 (file)
@@ -14,10 +14,15 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\Controller;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
 use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
 use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
+use TYPO3\CMS\Extbase\Mvc\RequestInterface;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3Fluid\Fluid\View\AbstractTemplateView;
+use TYPO3Fluid\Fluid\View\TemplateView;
 
 /**
  * Test case
@@ -540,4 +545,67 @@ class ActionControllerTest extends \TYPO3\Components\TestingFramework\Core\Unit\
             ],
         ];
     }
+
+    /**
+     * @param TemplateView $viewMock
+     * @param string|null $expectedHeader
+     * @param string|null $expectedFooter
+     * @test
+     * @dataProvider headerAssetDataProvider
+     */
+    public function rendersAndAssignsAssetsFromViewIntoPageRenderer($viewMock, $expectedHeader, $expectedFooter)
+    {
+        $this->mockObjectManager = $this->getMockBuilder(ObjectManager::class)->setMethods(['get'])->getMock();
+        $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->setMethods(['addHeaderData', 'addFooterData'])->getMock();
+        if (!$viewMock instanceof TemplateView) {
+            $this->mockObjectManager->expects($this->never())->method('get');
+        } else {
+            $this->mockObjectManager->expects($this->any())->method('get')->with(PageRenderer::class)->willReturn($pageRendererMock);
+        }
+        if (!empty(trim($expectedHeader))) {
+            $pageRendererMock->expects($this->once())->method('addHeaderData')->with($expectedHeader);
+        } else {
+            $pageRendererMock->expects($this->never())->method('addHeaderData');
+        }
+        if (!empty(trim($expectedFooter))) {
+            $pageRendererMock->expects($this->once())->method('addFooterData')->with($expectedFooter);
+        } else {
+            $pageRendererMock->expects($this->never())->method('addFooterData');
+        }
+        $requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass();
+        $subject = new ActionController();
+        $viewProperty = new \ReflectionProperty($subject, 'view');
+        $viewProperty->setAccessible(true);
+        $viewProperty->setValue($subject, $viewMock);
+        $objectManagerProperty = new \ReflectionProperty($subject, 'objectManager');
+        $objectManagerProperty->setAccessible(true);
+        $objectManagerProperty->setValue($subject, $this->mockObjectManager);
+
+        $method = new \ReflectionMethod($subject, 'renderAssetsForRequest');
+        $method->setAccessible(true);
+        $method->invokeArgs($subject, [$requestMock]);
+    }
+
+    /**
+     * @return array
+     */
+    public function headerAssetDataProvider()
+    {
+        $viewWithHeaderData = $this->getMockBuilder(TemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
+        $viewWithHeaderData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn('custom-header-data');
+        $viewWithHeaderData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn(null);
+        $viewWithFooterData = $this->getMockBuilder(TemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
+        $viewWithFooterData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn(null);
+        $viewWithFooterData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn('custom-footer-data');
+        $viewWithBothData = $this->getMockBuilder(TemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
+        $viewWithBothData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn('custom-header-data');
+        $viewWithBothData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn('custom-footer-data');
+        $invalidView = $this->getMockBuilder(AbstractTemplateView::class)->disableOriginalConstructor()->getMockForAbstractClass();
+        return [
+            [$viewWithHeaderData, 'custom-header-data', null],
+            [$viewWithFooterData, null, 'custom-footer-data'],
+            [$viewWithBothData, 'custom-header-data', 'custom-footer-data'],
+            [$invalidView, null, null]
+        ];
+    }
 }