Skip to content
Snippets Groups Projects
Commit 067d81a4 authored by Jonas Eberle's avatar Jonas Eberle Committed by Georg Ringer
Browse files

[FEATURE] Introduce AssetRenderer BeforeRendering events

AssetRenderer gets events for manipulating AssetCollector assets.

This enables asset post-processing extensions like EXT:min or
EXT:ws_scss to fully support AssetCollector assets.

The AssetCollector::add*() methods get an optional parameter that
allows filtering for (non-)priority assets. It became apararent during
testing that any listener would otherwise have to implement that
itself in order to avoid double-processing assets (as there are two
rendering passes, priority and non-priority).

Resolves: #90899
Releases: master
Change-Id: I2526328d3ee4ab269fa654347abd6156ee516a84
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64021


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: default avatarBenni Mack <benni@typo3.org>
Reviewed-by: default avatarGeorg Ringer <georg.ringer@gmail.com>
parent 11c53cbc
Branches
Tags
No related merge requests found
Showing
with 411 additions and 28 deletions
......@@ -204,23 +204,42 @@ class AssetCollector implements SingletonInterface
return $this->media;
}
public function getJavaScripts(): array
public function getJavaScripts(?bool $priority = null): array
{
return $this->javaScripts;
return $this->filterAssetsPriority($this->javaScripts, $priority);
}
public function getInlineJavaScripts(): array
public function getInlineJavaScripts(?bool $priority = null): array
{
return $this->inlineJavaScripts;
return $this->filterAssetsPriority($this->inlineJavaScripts, $priority);
}
public function getStyleSheets(): array
public function getStyleSheets(?bool $priority = null): array
{
return $this->styleSheets;
return $this->filterAssetsPriority($this->styleSheets, $priority);
}
public function getInlineStyleSheets(): array
public function getInlineStyleSheets(?bool $priority = null): array
{
return $this->inlineStyleSheets;
return $this->filterAssetsPriority($this->inlineStyleSheets, $priority);
}
/**
* @param array $assets Takes a reference to prevent a deep copy. The variable is not changed (const).
* @param bool|null $priority null: no filter; else filters for the given priority
* @return array
*/
protected function filterAssetsPriority(array &$assets, ?bool $priority): array
{
if ($priority === null) {
return $assets;
}
$currentPriorityAssets = [];
foreach ($assets as $identifier => $asset) {
if ($priority === ($asset['options']['priority'] ?? false)) {
$currentPriorityAssets[$identifier] = $asset;
}
}
return $currentPriorityAssets;
}
}
......@@ -16,11 +16,14 @@ namespace TYPO3\CMS\Core\Page;
* The TYPO3 project - inspiring people to share!
*/
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent;
use TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Class AssetRenderer
* @internal The AssetRenderer is used for the asset rendering and is not public API
*/
class AssetRenderer
......@@ -30,54 +33,73 @@ class AssetRenderer
*/
protected $assetCollector;
public function __construct(AssetCollector $assetCollector = null)
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
public function __construct(AssetCollector $assetCollector = null, EventDispatcherInterface $eventDispatcher = null)
{
$this->assetCollector = $assetCollector ?? GeneralUtility::makeInstance(AssetCollector::class);
$this->eventDispatcher = $eventDispatcher ?? GeneralUtility::makeInstance(EventDispatcher::class);
}
public function renderInlineJavaScript($priority = false): string
{
$this->eventDispatcher->dispatch(
new BeforeJavaScriptsRenderingEvent($this->assetCollector, true, $priority)
);
$template = '<script%attributes%>%source%</script>';
$assets = $this->assetCollector->getInlineJavaScripts();
return $this->render($assets, $template, $priority);
$assets = $this->assetCollector->getInlineJavaScripts($priority);
return $this->render($assets, $template);
}
public function renderJavaScript($priority = false): string
{
$this->eventDispatcher->dispatch(
new BeforeJavaScriptsRenderingEvent($this->assetCollector, false, $priority)
);
$template = '<script%attributes%></script>';
$assets = $this->assetCollector->getJavaScripts();
$assets = $this->assetCollector->getJavaScripts($priority);
foreach ($assets as &$assetData) {
$assetData['attributes']['src'] = $this->getAbsoluteWebPath($assetData['source']);
}
return $this->render($assets, $template, $priority);
return $this->render($assets, $template);
}
public function renderInlineStyleSheets($priority = false): string
{
$this->eventDispatcher->dispatch(
new BeforeStylesheetsRenderingEvent($this->assetCollector, true, $priority)
);
$template = '<style%attributes%>%source%</style>';
$assets = $this->assetCollector->getInlineStyleSheets();
return $this->render($assets, $template, $priority);
$assets = $this->assetCollector->getInlineStyleSheets($priority);
return $this->render($assets, $template);
}
public function renderStyleSheets(bool $priority = false, string $endingSlash = ''): string
{
$this->eventDispatcher->dispatch(
new BeforeStylesheetsRenderingEvent($this->assetCollector, false, $priority)
);
$template = '<link%attributes% ' . $endingSlash . '>';
$assets = $this->assetCollector->getStyleSheets();
$assets = $this->assetCollector->getStyleSheets($priority);
foreach ($assets as &$assetData) {
$assetData['attributes']['href'] = $this->getAbsoluteWebPath($assetData['source']);
$assetData['attributes']['rel'] = $assetData['attributes']['rel'] ?? 'stylesheet';
$assetData['attributes']['type'] = $assetData['attributes']['type'] ?? 'text/css';
}
return $this->render($assets, $template, $priority);
return $this->render($assets, $template);
}
protected function render(array $assets, string $template, bool $priority = false): string
protected function render(array $assets, string $template): string
{
$results = [];
foreach ($assets as $assetData) {
if (($assetData['options']['priority'] ?? false) !== $priority) {
continue;
}
$attributes = $assetData['attributes'];
$attributesString = count($attributes) ? ' ' . GeneralUtility::implodeAttributes($attributes, true) : '';
$results[] = str_replace(
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Page\Event;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Page\AssetCollector;
abstract class AbstractBeforeAssetRenderingEvent
{
/**
* @var AssetCollector
*/
protected $assetCollector;
/**
* @var bool
*/
protected $inline;
/**
* @var bool
*/
protected $priority;
public function getAssetCollector(): AssetCollector
{
return $this->assetCollector;
}
public function isInline(): bool
{
return $this->inline;
}
public function isPriority(): bool
{
return $this->priority;
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Page\Event;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Page\AssetCollector;
/**
* This event is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]JavaScript renders the output.
*/
final class BeforeJavaScriptsRenderingEvent extends AbstractBeforeAssetRenderingEvent
{
public function __construct(AssetCollector $assetCollector, bool $isInline, bool $priority)
{
$this->assetCollector = $assetCollector;
$this->inline = $isInline;
$this->priority = $priority;
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Page\Event;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Page\AssetCollector;
/**
* This event is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]Stylesheets renders the output.
*/
final class BeforeStylesheetsRenderingEvent extends AbstractBeforeAssetRenderingEvent
{
public function __construct(AssetCollector $assetCollector, bool $isInline, bool $priority)
{
$this->assetCollector = $assetCollector;
$this->inline = $isInline;
$this->priority = $priority;
}
}
......@@ -329,6 +329,11 @@ services:
method: 'addCategoryDatabaseSchema'
event: TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent
TYPO3\CMS\Core\Page\AssetRenderer:
public: true
arguments:
$eventDispatcher: '@Psr\EventDispatcher\EventDispatcherInterface'
# clean up files
TYPO3\CMS\Core\Resource\Processing\FileDeletionAspect:
tags:
......
.. include:: ../../Includes.txt
.. _changelog-Feature-90522-IntroduceAssetCollector:
==========================================
Feature: #90522 - Introduce AssetCollector
==========================================
......@@ -41,10 +43,10 @@ The new API
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeStyleSheet(string $identifier): self`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeInlineStyleSheet(string $identifier): self`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeMedia(string $identifier): self`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getJavaScripts(): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineJavaScripts(): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getStyleSheets(): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineStyleSheets(): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getJavaScripts(?bool $priority = null): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineJavaScripts(?bool $priority = null): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getStyleSheets(?bool $priority = null): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineStyleSheets(?bool $priority = null): array`
- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getMedia(): array`
New ViewHelpers
......@@ -74,7 +76,12 @@ Currently, assets registered with the AssetCollector are not included in callbac
- :php:`$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssConcatenateHandler']`
- :php:`$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsConcatenateHandler']`
Currently, CSS and JavaScript registered with the AssetCollector will always be rendered after their
.. versionadded:: 10.4
Events for the new API have been introduced in
:ref:`changelog-Feature-90899-IntroduceAssetPreRenderingEvents`
Currently, CSS and JavaScript registered with the AssetCollector will be rendered after their
PageRenderer counterparts. The order is:
- :html:`<head>`
......
.. include:: ../../Includes.txt
.. _changelog-Feature-90899-IntroduceAssetPreRenderingEvents:
==============================================================
Feature: #90899 - Introduce AssetRenderer pre-rendering events
==============================================================
See :issue:`90899`
Description
===========
AssetRenderer is amended by two events which allow post-processing of
AssetCollector assets.
These new PSR-14 events are introduced:
.. code-block:: php
\TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent
\TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent
Both stem fom the abstract base class
`\TYPO3\CMS\Core\Page\Event\AbstractBeforeAssetRenderingEvent` and provide
these public methods:
.. code-block:: php
getAssetCollector(): AssetCollector
isInline(): bool
isPriority(): bool
:php:`inline` and :php:`priority` refer to how the asset was registered with
:ref:`AssetCollector <changelog-Feature-90522-IntroduceAssetCollector>`.
The events are fired exactly once for every combination of
:php:`inline`/:php:`priority` before the corresponding section of JS/CSS assets
are rendered by the AssetRenderer.
To make the events easier to use, the :php:`AssetCollector::get*()` methods
have gotten an optional parameter `?bool $priority = null` which when given a
boolean only returns assets of the given priority.
.. note::
Note: post-processing functionality for assets registered via
TypoScript :ts:`page.include...` or the :php:`PageRenderer::add*()`
functions are still provided by these hooks:
.. code-block:: php
$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssCompressHandler']
$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsCompressHandler']
$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssConcatenateHandler']
$GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsConcatenateHandler']
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']
['t3lib/class.t3lib_pagerenderer.php']['render-preProcess']
Assets registered with the AssetCollector (and output through the
AssetRenderer) are not included in those.
Example
=======
As an example let's make sure jQuery is included in a specific version and
from a CDN.
.. rst-class:: bignums
1. Register our listeners
:file:`Configuration/Services.yaml`
.. code-block:: yaml
services:
MyVendor\MyExt\EventListener\AssetRenderer\LibraryVersion:
tags:
- name: event.listener
identifier: 'myExt/LibraryVersion'
event: TYPO3\CMS\Core\Page\Event\AssetRendererBeforeRenderingEvent
2. Implement Listener to enforce a library version or CDN URI
.. code-block:: php
namespace MyVendor\MyExt\EventListener\AssetRenderer;
use TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent;
/**
* If a library has been registered, it is made sure that it is loaded
* from the given URI
*/
class LibraryVersion
{
protected $libraries = [
'jquery' => 'https://code.jquery.com/jquery-3.4.1.min.js',
];
public function __invoke(BeforeJavaScriptsRenderingEvent $event): void
{
if ($event->isInline()) {
return;
}
foreach ($this->libraries as $library => $source) {
$asset = $event->getAssetCollector()->getJavaScripts($event->isPriority())
// if it was already registered
if ($asset[$library] ?? false) {
// we set our authoritative version
$event->getAssetCollector()->addJavaScript($library, $source);
}
}
}
}
Impact
======
Existing installations are not affected.
If using the AssetCollector API, these new events should be used for asset
postprocessing.
Related
=======
- :ref:`changelog-Feature-90522-IntroduceAssetCollector`
.. _examples: https://typo3.org
.. index:: PHP-API, ext:core
......@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace TYPO3\CMS\Core\Tests\Unit\Page;
use TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent;
use TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent;
class AssetDataProvider
{
public static function filesDataProvider(): array
......@@ -439,4 +442,22 @@ class AssetDataProvider
],
];
}
/**
* cross-product of all combinations of AssetRenderer::render*() methods and priorities
* @return array[] [render method name, isInline, isPriority, event class]
*/
public static function renderMethodsAndEventsDataProvider(): array
{
return [
['renderInlineJavaScript', true, true, BeforeJavaScriptsRenderingEvent::class],
['renderInlineJavaScript', true, false, BeforeJavaScriptsRenderingEvent::class],
['renderJavaScript', false, true, BeforeJavaScriptsRenderingEvent::class],
['renderJavaScript', false, false, BeforeJavaScriptsRenderingEvent::class],
['renderInlineStylesheets', true, true, BeforeStylesheetsRenderingEvent::class],
['renderInlineStylesheets', true, false, BeforeStylesheetsRenderingEvent::class],
['renderStylesheets', false, true, BeforeStylesheetsRenderingEvent::class],
['renderStylesheets', false, false, BeforeStylesheetsRenderingEvent::class],
];
}
}
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace TYPO3\CMS\Core\Tests\Unit\Page;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Page\AssetRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -16,11 +17,21 @@ class AssetRendererTest extends UnitTestCase
*/
protected $assetRenderer;
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
public function setUp(): void
{
parent::setUp();
$this->resetSingletonInstances = true;
$this->assetRenderer = GeneralUtility::makeInstance(AssetRenderer::class);
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->assetRenderer = GeneralUtility::makeInstance(
AssetRenderer::class,
null,
$this->eventDispatcher
);
}
/**
......@@ -90,4 +101,32 @@ class AssetRendererTest extends UnitTestCase
self::assertSame($expectedMarkup['css_no_prio'], $this->assetRenderer->renderInlineStyleSheets());
self::assertSame($expectedMarkup['css_prio'], $this->assetRenderer->renderInlineStyleSheets(true));
}
/**
* @param string $renderMethodName
* @param bool $isInline
* @param bool $priority
* @param string $eventClassName
* @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::renderMethodsAndEventsDataProvider
*/
public function testBeforeRenderingEvent(
string $renderMethodName,
bool $isInline,
bool $priority,
string $eventClassName
): void {
$assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
$event = new $eventClassName(
$assetCollector,
$isInline,
$priority
);
$this->eventDispatcher
->expects(self::once())
->method('dispatch')
->with($event);
$this->assetRenderer->$renderMethodName($priority);
}
}
......@@ -16,10 +16,13 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\Controller;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Cache\Backend\NullBackend;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Http\ServerRequestFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
......@@ -102,6 +105,18 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
])->disableOriginalConstructor()
->getMock();
$tsfe->expects(self::exactly(2))->method('processNonCacheableContentPartsAndSubstituteContentMarkers')->willReturnCallback([$this, 'processNonCacheableContentPartsAndSubstituteContentMarkers']);
/**
* prepare an EventDispatcher for ::makeInstance(AssetRenderer)
* @see \TYPO3\CMS\Core\Page\PageRenderer::renderJavaScriptAndCss
*/
GeneralUtility::setSingletonInstance(
EventDispatcher::class,
new EventDispatcher(
new ListenerProvider($this->createMock(ContainerInterface::class))
)
);
$tsfe->content = file_get_contents(__DIR__ . '/Fixtures/renderedPage.html');
$config = [
'INTincScript_ext' => [
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment