Skip to content
Snippets Groups Projects
Commit 4d6898f6 authored by Luke Leber's avatar Luke Leber
Browse files

Issue #3254011 by Luke.Leber: Allow more granular activation of module decorator

parent 1d683cf8
Branches
Tags
1 merge request!2Issue #3254011: Allow more granular activation of module decorator
enabled: false
enabled_themes: { }
inline_all_css.settings:
type: config_object
mapping:
enabled:
type: boolean
label: 'Transform external stylesheets into inline styles'
enabled_themes:
type: sequence
label: 'Enabled themes'
sequence:
type: string
label: 'Enabled theme'
inline_all_css.config:
title: Inline all css configuration
description: 'Configure inline all css.'
parent: system.admin_config_system
route_name: inline_all_css.config
'administer inline all css':
title: 'Administer inline all css'
description: 'Administer inline all css settings.'
restrict access: true
inline_all_css.config:
path: '/admin/config/system/inline_all_css'
defaults:
_form: '\Drupal\inline_all_css\Form\SettingsForm'
_title: 'Inline all css configuration'
requirements:
_permission: 'administer inline all css'
......@@ -4,5 +4,8 @@ services:
class: Drupal\inline_all_css\Asset\CriticalCssCollectionRenderer
decorates: asset.css.collection_renderer
arguments:
- '@asset.css.collection_renderer.inline_all_css.inner'
- '@config.factory'
- '@theme.manager'
- '@file_system'
- '@event_dispatcher'
......@@ -3,8 +3,10 @@
namespace Drupal\inline_all_css\Asset;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\inline_all_css\Event\CssPreRenderEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use function file_get_contents;
......@@ -16,6 +18,27 @@ use function file_get_contents;
*/
class CriticalCssCollectionRenderer implements AssetCollectionRendererInterface {
/**
* The decorated collection renderer service.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $cssCollectionRenderer;
/**
* The module configuration.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The theme manager service.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The filesystem service.
*
......@@ -33,20 +56,35 @@ class CriticalCssCollectionRenderer implements AssetCollectionRendererInterface
/**
* Constructs a CriticalCssCollectionRenderer.
*
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
* The decorated asset renderer service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
*/
public function __construct(FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher) {
public function __construct(AssetCollectionRendererInterface $css_collection_renderer, ConfigFactoryInterface $config_factory, ThemeManagerInterface $theme_manager, FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher) {
$this->cssCollectionRenderer = $css_collection_renderer;
$this->config = $config_factory->get('inline_all_css.settings');
$this->themeManager = $theme_manager;
$this->fileSystem = $file_system;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
* Generates an inline style element from the provided assets.
*
* @param array $assets
* An asset collection.
*
* @return array
* An inline style tag element.
*/
public function render(array $assets) {
protected function getInlineCss(array $assets) {
$css = '';
foreach ($assets as $asset) {
$file = $this->fileSystem->realpath($asset['data']);
......@@ -59,12 +97,33 @@ class CriticalCssCollectionRenderer implements AssetCollectionRendererInterface
$css = $event->getCss();
return [
[
'#type' => 'html_tag',
'#tag' => 'style',
'#value' => Markup::create($css),
],
'#type' => 'html_tag',
'#tag' => 'style',
'#value' => Markup::create($css),
];
}
/**
* {@inheritdoc}
*/
public function render(array $assets) {
$elements = [];
if ($this->config->get('enabled') === TRUE) {
$enabled_themes = $this->config->get('enabled_themes');
$active_theme = $this->themeManager->getActiveTheme()->getName();
if (empty($enabled_themes) || in_array($active_theme, $enabled_themes, TRUE)) {
$elements[] = $this->getInlineCss($assets);
}
else {
$elements = $this->cssCollectionRenderer->render($assets);
}
}
else {
$elements = $this->cssCollectionRenderer->render($assets);
}
return $elements;
}
}
<?php
namespace Drupal\inline_all_css\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Settings form for the inline all css module.
*/
class SettingsForm extends ConfigFormBase {
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->themeHandler = $container->get('theme_handler');
return $instance;
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['inline_all_css.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'inline_all_css_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('inline_all_css.settings');
$form['warning'] = [
'#type' => 'markup',
'#markup' => <<<HTML
<p class="messages messages--warning">{$this->t('If there is a content security policy in place, please be sure to test this setting before enabling on production!')}</p>
HTML,
];
$form['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable'),
'#default_value' => $config->get('enabled'),
];
$themes = $this->themeHandler->listInfo();
foreach ($themes as $k => $theme) {
$themes[$k] = $theme->getName();
}
$form['enabled_themes'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Enabled themes'),
'#description' => $this->t('If no themes are selected, css will be inlined on all themes'),
'#options' => $themes,
'#multiple' => TRUE,
'#default_value' => $config->get('enabled_themes'),
'#states' => [
'visible' => [
':input[name="enabled"]' => ['checked' => TRUE],
],
],
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('inline_all_css.settings');
$config->set('enabled', $form_state->getValue('enabled'));
$enabled_themes = $form_state->getValue('enabled_themes');
// Only take the values.
$enabled_themes = array_values($enabled_themes);
// Filter out any empties.
$enabled_themes = array_filter($enabled_themes);
$config->set('enabled_themes', $enabled_themes);
$config->save();
parent::submitForm($form, $form_state);
}
}
<?php
namespace Drupal\Tests\inline_all_css\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Test cases for the settings form.
*
* @group inline_all_css
*/
class SettingsFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'inline_all_css',
];
/**
* Test case for the settings form.
*/
public function testSettingsForm() {
$session = $this->getSession();
// Ensure that access is denied by default.
$this->drupalGet(Url::fromRoute('inline_all_css.config'));
static::assertEquals(403, $session->getStatusCode());
// Ensure that access is granted with the proper permission.
$this->drupalLogin($this->drupalCreateUser(['administer inline all css']));
$this->drupalGet(Url::fromRoute('inline_all_css.config'));
static::assertEquals(200, $session->getStatusCode());
$page = $session->getPage();
$assert = $this->assertSession();
// Ensure the proper initial form state.
$assert->pageTextContains('If there is a content security policy in place, please be sure to test this setting before enabling on production!');
$enabled = $page->findField('Enable');
static::assertFalse($enabled->isChecked());
// Save some settings.
$enabled->check();
$page->findField('stark')->check();
$page->findButton('Save configuration')->press();
$assert->pageTextContains('The configuration options have been saved.');
// Ensure that the new form state matches expectations.
$enabled = $page->findField('Enable');
$stark = $page->findField('stark');
static::assertTrue($enabled->isChecked());
static::assertTrue($stark->isChecked());
// Ensure that the configuration itself is in the right state.
$config = \Drupal::config('inline_all_css.settings');
static::assertTrue($config->get('enabled'));
static::assertEquals(['stark'], $config->get('enabled_themes'));
}
}
......@@ -2,8 +2,13 @@
namespace Drupal\Tests\inline_all_css\Unit;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Theme\ActiveTheme;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\inline_all_css\Asset\CriticalCssCollectionRenderer;
use Drupal\inline_all_css\Event\CssPreRenderEvent;
use Drupal\Tests\UnitTestCase;
......@@ -16,6 +21,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class CssCollectionRendererTest extends UnitTestCase {
protected const ASSETS = [
['data' => 'public://test-1.css'],
['data' => 'public://test-2.css'],
['data' => 'public://test-3.css'],
];
/**
* The subject under test.
*
......@@ -23,6 +34,27 @@ class CssCollectionRendererTest extends UnitTestCase {
*/
protected $instance;
/**
* The mocked css collection renderer.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $cssCollectionRenderer;
/**
* The mocked theme manager service.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $themeManager;
/**
* The mock config.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* {@inheritdoc}
*/
......@@ -48,25 +80,131 @@ class CssCollectionRendererTest extends UnitTestCase {
->willReturnCallback(static function (CssPreRenderEvent $event) {
$css = $event->getCss();
$css .= <<<CSS
.event {
p {
color: gray;
}
CSS;
$event->setCss($css);
});
$this->instance = new CriticalCssCollectionRenderer($filesystem, $eventDispatcher);
$this->cssCollectionRenderer = $this->getMockBuilder(AssetCollectionRendererInterface::class)
->disableOriginalConstructor()
->getMock();
$this->themeManager = $this->getMockBuilder(ThemeManagerInterface::class)
->disableOriginalConstructor()
->getMock();
$config_factory = $this->getMockBuilder(ConfigFactoryInterface::class)
->disableOriginalConstructor()
->getMock();
$this->config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
$config_factory
->method('get')
->willReturn($this->config);
$this->instance = new CriticalCssCollectionRenderer($this->cssCollectionRenderer, $config_factory, $this->themeManager, $filesystem, $eventDispatcher);
}
/**
* Sets the mock module configuration.
*
* @param bool $enabled
* Should inlining be enabled?
* @param string[] $enabled_themes
* An array of themes to enable inlining for.
*/
protected function setConfig($enabled, array $enabled_themes) {
$this->config
->method('get')
->willReturnMap([
['enabled', $enabled],
['enabled_themes', $enabled_themes],
]);
}
/**
* Sets the current active theme.
*
* @param string $theme
* The theme to set.
*/
protected function setActiveTheme($theme) {
$active_theme = $this->getMockBuilder(ActiveTheme::class)
->disableOriginalConstructor()
->getMock();
$active_theme
->method('getName')
->willReturn($theme);
$this->themeManager
->method('getActiveTheme')
->willReturn($active_theme);
}
/**
* Test case for when the enabled flag is not set.
*/
public function testCssCollectionRendererDisabled() {
$this->setConfig(FALSE, []);
// The decorated service should be called.
$this->cssCollectionRenderer
->expects(static::once())
->method('render');
$this->instance->render(static::ASSETS);
}
/**
* Test case for when the enabled flag is set, but the theme is wrong.
*/
public function testCssCollectionRendererWrongTheme() {
$this->setConfig(TRUE, ['some_theme']);
$this->setActiveTheme('a_different_theme');
// The decorated service should be called.
$this->cssCollectionRenderer
->expects(static::once())
->method('render');
$this->instance->render(static::ASSETS);
}
/**
* Test case for when the enabled flag is set and all themes are enabled.
*/
public function testCssCollectionRendererAllThemes() {
$this->setConfig(TRUE, []);
$this->setActiveTheme('should_not_matter');
// The decorated service should be called.
$this->cssCollectionRenderer
->expects(static::never())
->method('render');
$this->instance->render(static::ASSETS);
}
/**
* Test case for inline CSS rendering.
*/
public function testCssCollectionRenderer() {
$assets = [
['data' => 'public://test-1.css'],
['data' => 'public://test-2.css'],
['data' => 'public://test-3.css'],
];
public function testCssCollectionRendererEnabled() {
$this->setConfig(TRUE, ['my_theme']);
$this->setActiveTheme('my_theme');
// The decorated service should not be called.
$this->cssCollectionRenderer
->expects(static::never())
->method('render');
$expected = [
[
......@@ -82,13 +220,13 @@ html {
body {
color: blue;
}
.event {
p {
color: gray;
}
CSS),
],
];
$actual = $this->instance->render($assets);
$actual = $this->instance->render(static::ASSETS);
static::assertEquals($expected, $actual);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment