diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 8eb1ec913928bfbdd7f0cadc6ed21bfb7a4657c2..bd0db8db8103791d9677889a5bf740fb79b902ef 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -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()); + } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php new file mode 100644 index 0000000000000000000000000000000000000000..15b8d73cd29c286edd2fa710ee9c862234d9b8f3 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php @@ -0,0 +1,44 @@ +<?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']); + } + } + } + } + +} diff --git a/core/modules/system/src/Form/DevelopmentSettingsForm.php b/core/modules/system/src/Form/DevelopmentSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..282d0f02cf2face6b11721da415bc0b0a9f2f728 --- /dev/null +++ b/core/modules/system/src/Form/DevelopmentSettingsForm.php @@ -0,0 +1,152 @@ +<?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.')); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index c6513010faa03770c6ca3ab289e643e84f97a87a..635f152ea214eb927efe395c3796bb35f1c7cb17 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -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; } diff --git a/core/modules/system/system.links.menu.yml b/core/modules/system/system.links.menu.yml index ad012550dc85526f564566ae855388df7860edd7..04357e8d57f1de22ddaf7b024d8a64e38bf10db4 100644 --- a/core/modules/system/system.links.menu.yml +++ b/core/modules/system/system.links.menu.yml @@ -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 diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 9e01d58469e7f566a677b613c9d5f59a742906ee..728b5c4f857f341d226752dbf10da23fca4aef6a 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -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: diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..acd2899059d52563227276abfa8a59ad2bac9607 --- /dev/null +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php @@ -0,0 +1,135 @@ +<?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")); + } + } + +}