Skip to content
Snippets Groups Projects
Verified Commit d7663e8b authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3278493 by mglaman, rabbitlair, mherchel, Purencool, dww, benjifisher,...

Issue #3278493 by mglaman, rabbitlair, mherchel, Purencool, dww, benjifisher, lauriii, andy-blum, AdamPS, jwilson3, alexpott, smustgrave, rlahoda, akalata, ckrina, bnjmnm, crasx, mndonx, hbrokmeier, callinmullaney: Make it easier for theme builders to enable Twig debugging and disable render cache

(cherry picked from commit da2eaec5)
parent 8716f25e
No related branches found
No related tags found
54 merge requests!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4686Issue #3292350: file_validate_image_resolution does not update file size after resizing,!4594Applying patch for Views Global Text area field to allow extra HTML tags. As video, source and iframe tag is not rendering. Due to which Media embedded video and remote-video not rendering in Views Global Text area field.,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!4114Issue #2707291: Disable body-level scrolling when a dialog is open as a modal,!4034Issue #3308432: The link on the Image tag is redirecting to an undefined page from the node preview screen.,!4022Update String overrides example in default.settings.php,!3948Issue #3358600: [meta] Fix PHPStan L1 errors "Variable $foo might not be defined.",!3878Removed unused condition head title for views,!38582585169-10.1.x,!3825Issue #2972573: randomMachineName() should conform to processMachineName() pattern,!3818Issue #2140179: $entity->original gets stale between updates,!3770Issue #3318112: Move "Block layout" from Structure to Appearance,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3699Resolve #2953566 "Allow entities to",!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3451Issue #2410579: Allows setting the current language programmatically.,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3228Issue #2920678: Add config validation for the allowed characters of machine names,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!31312878513-10.1.x,!3009Issue #3323252: Add @method PhpDoc for EntityStorageInterface descendants,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
......@@ -9,6 +9,7 @@
use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass;
use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass;
use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass;
use Drupal\Core\DependencyInjection\Compiler\DevelopmentSettingsPass;
use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass;
use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
......@@ -58,6 +59,8 @@ public function register(ContainerBuilder $container) {
// list-building passes are operating on the post-alter services list.
$container->addCompilerPass(new ModifyServiceDefinitionsPass());
$container->addCompilerPass(new DevelopmentSettingsPass());
$container->addCompilerPass(new ProxyServicesPass());
$container->addCompilerPass(new BackendCompilerPass());
......@@ -93,6 +96,7 @@ public function register(ContainerBuilder $container) {
$container->addCompilerPass(new PluginManagerPass());
$container->addCompilerPass(new DeprecatedServicePass());
}
/**
......
<?php
namespace Drupal\Core\DependencyInjection\Compiler;
use Drupal\Core\Cache\NullBackendFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Defines a compiler pass to register development settings.
*/
class DevelopmentSettingsPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
/** @var \Drupal\Core\State\StateInterface $state */
$state = $container->get('state');
$twig_debug = $state->get('twig_debug', FALSE);
$twig_cache_disable = $state->get('twig_cache_disable', FALSE);
if ($twig_debug || $twig_cache_disable) {
$twig_config = $container->getParameter('twig.config');
$twig_config['debug'] = $twig_debug;
$twig_config['cache'] = !$twig_cache_disable;
$container->setParameter('twig.config', $twig_config);
}
if ($state->get('disable_rendered_output_cache_bins', FALSE)) {
$cache_bins = ['page', 'dynamic_page_cache', 'render'];
if (!$container->hasDefinition('cache.backend.null')) {
$container->register('cache.backend.null', NullBackendFactory::class);
}
foreach ($cache_bins as $cache_bin) {
if ($container->has("cache.$cache_bin")) {
$container->getDefinition("cache.$cache_bin")
->clearTag('cache.bin')
->addTag('cache.bin', ['default_backend' => 'cache.backend.null']);
}
}
}
}
}
<?php
namespace Drupal\system\Form;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure development settings for this site.
*
* @internal
*/
class DevelopmentSettingsForm extends FormBase {
/**
* Constructs a new development settings form.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal kernel.
*/
public function __construct(
protected StateInterface $state,
protected DrupalKernelInterface $kernel
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = new static(
$container->get('state'),
$container->get('kernel')
);
$instance->setMessenger($container->get('messenger'));
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'development_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['description'] = [
'#plain_text' => $this->t('These settings should only be enabled on development environments and never on production.'),
];
$twig_debug = $this->state->get('twig_debug', FALSE);
$twig_cache_disable = $this->state->get('twig_cache_disable', FALSE);
$twig_development_state_conditions = [
'input[data-drupal-selector="edit-twig-development-mode"]' => [
'checked' => TRUE,
],
];
$form['twig_development_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Twig development mode'),
'#description' => $this->t('Exposes Twig development settings.'),
'#default_value' => $twig_debug || $twig_cache_disable,
];
$form['twig_development'] = [
'#type' => 'fieldset',
'#title' => $this->t('Twig development mode'),
'#states' => [
'visible' => $twig_development_state_conditions,
],
];
$form['twig_development']['twig_debug'] = [
'#type' => 'checkbox',
'#title' => $this->t('Twig debug mode'),
'#description' => $this->t("Provides Twig's <code>dump()</code> function for debugging, outputs template suggestions to HTML comments, and automatically recompile Twig templates after changes."),
'#default_value' => $twig_debug,
];
$form['twig_development']['twig_cache_disable'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disable Twig cache'),
'#description' => $this->t('Twig templates are not cached and are always compiled when rendered.'),
'#default_value' => $twig_cache_disable,
];
if (!$twig_debug && !$twig_cache_disable) {
$form['twig_development']['twig_debug']['#states'] = [
'checked' => $twig_development_state_conditions,
];
$form['twig_development']['twig_cache_disable']['#states'] = [
'checked' => $twig_development_state_conditions,
];
}
$form['disable_rendered_output_cache_bins'] = [
'#type' => 'checkbox',
'#title' => $this->t('Do not cache markup'),
'#description' => $this->t('Disables render cache, dynamic page cache, and page cache.'),
'#default_value' => $this->state->get('disable_rendered_output_cache_bins', FALSE),
];
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save settings'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$disable_rendered_output_cache_bins_previous = $this->state->get('disable_rendered_output_cache_bins', FALSE);
$disable_rendered_output_cache_bins = (bool) $form_state->getValue('disable_rendered_output_cache_bins');
if ($disable_rendered_output_cache_bins) {
$this->state->set('disable_rendered_output_cache_bins', TRUE);
}
else {
$this->state->delete('disable_rendered_output_cache_bins');
}
$twig_development_mode = (bool) $form_state->getValue('twig_development_mode');
$twig_development_previous = $this->state->getMultiple(['twig_debug', 'twig_cache_disable']);
$twig_development = [
'twig_debug' => (bool) $form_state->getValue('twig_debug'),
'twig_cache_disable' => (bool) $form_state->getValue('twig_cache_disable'),
];
if ($twig_development_mode) {
$invalidate_container = $twig_development_previous !== $twig_development;
$this->state->setMultiple($twig_development);
}
else {
$invalidate_container = TRUE;
$this->state->deleteMultiple(array_keys($twig_development));
}
if ($invalidate_container || $disable_rendered_output_cache_bins_previous !== $disable_rendered_output_cache_bins) {
$this->kernel->invalidateContainer();
}
$this->messenger()->addStatus($this->t('The settings have been saved.'));
}
}
......@@ -1532,6 +1532,37 @@ function (callable $hook, string $module) use (&$module_list, $update_registry,
}
}
// Add warning when twig debug option is enabled.
if ($phase === 'runtime') {
$twig_debug = \Drupal::state()->get('twig_debug', FALSE);
$twig_cache_disable = \Drupal::state()->get('twig_cache_disable', FALSE);
if ($twig_debug || $twig_cache_disable) {
$requirements['twig_debug_enabled'] = [
'title' => t('Twig development mode'),
'value' => t('Twig development mode settings are turned on. Go to @link to disable them.', [
'@link' => Link::createFromRoute(
'development settings page',
'system.development_settings',
)->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
}
$render_cache_disabled = \Drupal::state()->get('disable_rendered_output_cache_bins', FALSE);
if ($render_cache_disabled) {
$requirements['render_cache_disabled'] = [
'title' => t('Markup caching disabled'),
'value' => t('Render cache, dynamic page cache, and page cache are bypassed. Go to @link to enable them.', [
'@link' => Link::createFromRoute(
'development settings page',
'system.development_settings',
)->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
}
}
return $requirements;
}
......
......@@ -76,6 +76,12 @@ system.performance_settings:
description: 'Configure caching and bandwidth optimization.'
route_name: system.performance_settings
weight: -20
system.development_settings:
title: Development settings
parent: system.admin_config_development
description: 'Configure theme development settings'
route_name: system.development_settings
weight: -19
system.logging_settings:
title: 'Logging and errors'
parent: system.admin_config_development
......
......@@ -182,6 +182,14 @@ system.performance_settings:
requirements:
_permission: 'administer site configuration'
system.development_settings:
path: '/admin/config/development/settings'
defaults:
_form: '\Drupal\system\Form\DevelopmentSettingsForm'
_title: 'Development settings'
requirements:
_permission: 'administer site configuration'
system.file_system_settings:
path: '/admin/config/media/file-system'
defaults:
......
<?php
namespace Drupal\Tests\system\FunctionalJavascript\Form;
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests development settings form items for expected behavior.
*
* @group Form
*/
class DevelopmentSettingsFormTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'dynamic_page_cache', 'page_cache'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer site configuration',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests turning on Twig development mode.
*
* @dataProvider twigDevelopmentData
*/
public function testTwigDevelopmentMode(bool $twig_development_mode, ?bool $twig_debug, ?bool $twig_cache_disable): void {
$twig_debug = $twig_debug ?? $twig_development_mode;
$twig_cache_disable = $twig_cache_disable ?? $twig_development_mode;
$twig_config = \Drupal::getContainer()->getParameter('twig.config');
self::assertFalse($twig_config['debug']);
self::assertNull($twig_config['auto_reload']);
self::assertTrue($twig_config['cache']);
$this->drupalGet(Url::fromRoute('system.development_settings'));
if ($twig_development_mode) {
$this->getSession()->getPage()->checkField('Twig development mode');
$this->assertSession()->checkboxChecked('Twig debug mode');
$this->assertSession()->checkboxChecked('Disable Twig cache');
}
if (!$twig_debug) {
$this->getSession()->getPage()->uncheckField('Twig debug mode');
}
if (!$twig_cache_disable) {
$this->getSession()->getPage()->uncheckField('Disable Twig cache');
}
$this->getSession()->getPage()->pressButton('Save settings');
$this->drupalGet(Url::fromRoute('system.status'));
if (!$twig_development_mode) {
$this->assertSession()->pageTextNotContains('Twig development mode');
}
else {
$this->assertSession()->pageTextContains('Twig development mode');
$this->assertSession()->linkExists('development settings page');
}
$refreshed_container = $this->initKernel(Request::create('/'));
$twig_config = $refreshed_container->getParameter('twig.config');
self::assertEquals($twig_debug, $twig_config['debug']);
self::assertNull($twig_config['auto_reload']);
self::assertEquals(!$twig_cache_disable, $twig_config['cache']);
}
/**
* Test data for Twig development mode.
*
* @return array[]
*/
public static function twigDevelopmentData(): array {
return [
'Twig development mode checked only' => [
TRUE,
NULL,
NULL,
],
'Twig debug mode only, keep Twig cache' => [
TRUE,
TRUE,
FALSE,
],
'Twig debug mode off, disable Twig cache' => [
TRUE,
FALSE,
TRUE,
],
'No changes' => [
FALSE,
NULL,
NULL,
],
];
}
/**
* Tests disabling cache bins which cache markup.
*/
public function testDisabledRenderedOutputCacheBins(): void {
self::assertFalse(\Drupal::getContainer()->has('cache.backend.null'));
$this->drupalGet(Url::fromRoute('system.development_settings'));
$this->getSession()->getPage()->checkField('Do not cache markup');
$this->getSession()->getPage()->pressButton('Save settings');
$this->drupalGet(Url::fromRoute('system.status'));
$this->assertSession()->pageTextContains('Markup caching disabled');
$this->assertSession()->linkExists('development settings page');
$refreshed_container = $this->initKernel(Request::create('/'));
self::assertTrue($refreshed_container->has('cache.backend.null'));
$cache_bins = ['page', 'dynamic_page_cache', 'render'];
foreach ($cache_bins as $cache_bin) {
self::assertInstanceOf(NullBackend::class, $refreshed_container->get("cache.$cache_bin"));
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment