<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Drupal Core</title>
    <description>Stay informed about developments in Drupal Core.</description>
    <link>https://www.drupal.org/project/drupal</link>
    <item>
      <title>#2381091: Improve example.gitignore </title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/2381091.md</link>
      <guid isPermaLink="false">019d8b24-2107-72bd-b458-9bd0217250b4</guid>
      <pubDate>Tue, 14 Apr 2026 08:04:02 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/59a2ce11bb3c9a53eb13980755d5d2e3292568e6"&gt;59a2ce1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/2381091"&gt;#2381091&lt;/a&gt; · 20 contributors · 75 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 24&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;example.gitignore&lt;/code&gt; template that Drupal ships for new sites now has more precise and complete rules for ignoring sensitive configuration files. The patterns for settings and services files are updated to cover files nested one subdirectory deeper inside each site directory, and explicit allow rules are added so that Drupal's own shipped template files (&lt;code&gt;default.settings.php&lt;/code&gt; and &lt;code&gt;default.services.yml&lt;/code&gt;) are never accidentally excluded. The alternative commented-out section for setups where &lt;code&gt;.gitignore&lt;/code&gt; lives inside the &lt;code&gt;sites/&lt;/code&gt; folder is also updated to cover services files and carry the matching allow rules, which it previously lacked entirely.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Two identical files are changed: &lt;code&gt;example.gitignore&lt;/code&gt; in the project root and &lt;code&gt;core/assets/scaffold/files/example.gitignore&lt;/code&gt; (the copy Composer scaffolding copies into a project). Both previously used the patterns &lt;code&gt;sites/*/settings*.php&lt;/code&gt; and &lt;code&gt;sites/*/services*.yml&lt;/code&gt;, which only matched files sitting directly inside a named site directory. That missed environment-specific or subdirectory-nested variants such as &lt;code&gt;sites/example.com/subdir/settings.local.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix replaces those patterns with &lt;code&gt;sites/*/*settings*.php&lt;/code&gt; and &lt;code&gt;sites/*/*services*.yml&lt;/code&gt;, adding one extra wildcard directory segment to cover one additional level of nesting inside each site directory. Because those broader globs would also match Drupal's own tracked template files (&lt;code&gt;sites/*/default.settings.php&lt;/code&gt; and &lt;code&gt;sites/*/default.services.yml&lt;/code&gt;), the commit immediately adds negation rules &lt;code&gt;!sites/*/default.settings.php&lt;/code&gt; and &lt;code&gt;!sites/*/default.services.yml&lt;/code&gt; after each group. Git negation rules re-include files that a preceding pattern would otherwise suppress, so the shipped templates remain visible to &lt;code&gt;git status&lt;/code&gt; for core contributors.&lt;/p&gt;
&lt;p&gt;The commented-out &amp;quot;sites/ folder&amp;quot; section — intended for sites where &lt;code&gt;.gitignore&lt;/code&gt; is placed inside &lt;code&gt;sites/&lt;/code&gt; rather than the project root — previously contained only &lt;code&gt;# */settings*.php&lt;/code&gt; and had no coverage for services files at all. The fix expands it to four lines: &lt;code&gt;# */*settings*.php&lt;/code&gt;, &lt;code&gt;# !*/default.settings.php&lt;/code&gt;, &lt;code&gt;# */*services*.yml&lt;/code&gt;, and &lt;code&gt;# !*/default.services.yml&lt;/code&gt;, making it consistent with the primary section. During review, &lt;code&gt;**&lt;/code&gt; (recursive double-star) was considered for the services pattern but rejected after it was pointed out that &lt;code&gt;**/*services*.yml&lt;/code&gt; would also silently suppress module-provided &lt;code&gt;services.yml&lt;/code&gt; files in a multisite layout. The final pattern uses &lt;code&gt;*/*&lt;/code&gt; (two explicit single-star segments) to limit the match depth. This issue was a follow-up to &lt;a href="https://www.drupal.org/node/2326913"&gt;#2326913&lt;/a&gt;, which first introduced services.yml patterns into the example gitignore.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: November 24, 2014&lt;/li&gt;
&lt;li&gt;Committed: April 14, 2026 (11 years later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mikeker&lt;/li&gt;
&lt;li&gt;dawehner&lt;/li&gt;
&lt;li&gt;alexpott&lt;/li&gt;
&lt;li&gt;jeanfei&lt;/li&gt;
&lt;li&gt;lluvigne (La Drupalera by Emergya)&lt;/li&gt;
&lt;li&gt;gnuget (Agaric)&lt;/li&gt;
&lt;li&gt;xjm (Zoocha)&lt;/li&gt;
&lt;li&gt;ankit_rathore (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;wim leers (Acquia)&lt;/li&gt;
&lt;li&gt;gengo_k&lt;/li&gt;
&lt;li&gt;quietone (PreviousNext)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3536964: run-tests.sh - segregate command line parsing and use Symfony Console classes</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3536964.md</link>
      <guid isPermaLink="false">019d8967-dfa7-7599-9197-f9f5c16068d5</guid>
      <category>Module maintainers</category>
      <pubDate>Mon, 13 Apr 2026 21:32:48 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; main&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/afcf2016aa03efabd302f6b9bd1aae0fabbb3d02"&gt;afcf201&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3536964"&gt;#3536964&lt;/a&gt; · 6 contributors · 53 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 9&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;run-tests.sh&lt;/code&gt; test runner script now uses Symfony Console's argument-parsing infrastructure instead of a hand-rolled parsing function. A new &lt;code&gt;Configuration&lt;/code&gt; class centralizes all command-line option handling, improving readability and moving the script closer to becoming a proper Symfony Console application command. For CI operators the most visible changes are: a substantially improved &lt;code&gt;--help&lt;/code&gt; output (automatically generated from the option definitions), the &lt;code&gt;--sqlite ':memory:'&lt;/code&gt; option now working (the old restriction is gone since &lt;a href="https://www.drupal.org/node/3515347"&gt;#3515347&lt;/a&gt; eliminated sub-process spawning), and the &lt;code&gt;--php&lt;/code&gt; option being deprecated and silently ignored. PHP binary detection now uses &lt;code&gt;PhpExecutableFinder&lt;/code&gt; from &lt;code&gt;symfony/process&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; &lt;code&gt;--php&lt;/code&gt; option in &lt;code&gt;run-tests.sh&lt;/code&gt; is now deprecated and silently ignored; PHP binary detection is handled automatically via &lt;code&gt;PhpExecutableFinder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;All changes are in &lt;code&gt;core/scripts/run-tests.sh&lt;/code&gt; and the new &lt;code&gt;core/tests/Drupal/TestTools/TestRunner/Configuration.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Previously, &lt;code&gt;run-tests.sh&lt;/code&gt; contained &lt;code&gt;simpletest_script_parse_args()&lt;/code&gt;, a hand-written function that iterated &lt;code&gt;$_SERVER['argv']&lt;/code&gt;, maintained a flat &lt;code&gt;$args&lt;/code&gt; array of defaults, and shared it as a global throughout the script. Every function accessed &lt;code&gt;global $args&lt;/code&gt; and then individual keys.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;Configuration&lt;/code&gt; final singleton class lives in the &lt;code&gt;Drupal\TestTools\TestRunner&lt;/code&gt; namespace. All options and their defaults are declared once in &lt;code&gt;Configuration::commandLineDefinition()&lt;/code&gt; using Symfony Console's &lt;code&gt;InputDefinition&lt;/code&gt;, &lt;code&gt;InputOption&lt;/code&gt;, and &lt;code&gt;InputArgument&lt;/code&gt; objects. The static factory &lt;code&gt;Configuration::createFromCommandLine()&lt;/code&gt; wraps the raw &lt;code&gt;argv&lt;/code&gt; in &lt;code&gt;ArgvInput&lt;/code&gt;, validates &lt;code&gt;--concurrency&lt;/code&gt;, splits the comma-separated &lt;code&gt;tests&lt;/code&gt; and &lt;code&gt;types&lt;/code&gt; values into arrays, and stores the result in the singleton. Access throughout the script is via &lt;code&gt;Config::get('option-name')&lt;/code&gt;, &lt;code&gt;Config::set('option-name', $value)&lt;/code&gt;, and &lt;code&gt;Config::getTests()&lt;/code&gt;. Functions that previously accepted &lt;code&gt;array $args&lt;/code&gt; as a parameter (&lt;code&gt;dump_tests_sequence()&lt;/code&gt;, &lt;code&gt;dump_bin_tests_sequence()&lt;/code&gt;) now call &lt;code&gt;Config::get()&lt;/code&gt; directly and drop the parameter.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;simpletest_script_init()&lt;/code&gt; is simplified: autoloader setup is moved to the top of the script (the &lt;code&gt;ClassLoader&lt;/code&gt; instance is passed in as a parameter), and the entire block that guessed the PHP binary from the &lt;code&gt;$_&lt;/code&gt; and &lt;code&gt;SUDO_COMMAND&lt;/code&gt; environment variables is removed. The startup banner now calls &lt;code&gt;(new PhpExecutableFinder())-&amp;gt;find()&lt;/code&gt; inline. The &lt;code&gt;--php&lt;/code&gt; option remains accepted so existing scripts do not break, but its value is never read and it is documented as deprecated.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;simpletest_script_setup_test_run_results_storage()&lt;/code&gt; loses its &lt;code&gt;$new&lt;/code&gt; boolean parameter; the caller always creates storage now.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;simpletest_script_help()&lt;/code&gt; drops the full hand-written option listing and instead calls &lt;code&gt;DescriptorHelper::describe()&lt;/code&gt; against the &lt;code&gt;InputDefinition&lt;/code&gt;, automatically producing consistent formatted help. Its signature changes to accept &lt;code&gt;InputDefinition $input_definition&lt;/code&gt;, &lt;code&gt;string $script_basename&lt;/code&gt;, and &lt;code&gt;ConsoleOutput $console_output&lt;/code&gt; instead of reading globals.&lt;/p&gt;
&lt;p&gt;A companion change in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; sets &lt;code&gt;CI_NODE_INDEX: 1&lt;/code&gt; and &lt;code&gt;CI_NODE_TOTAL: 1&lt;/code&gt; explicitly for the Build job, since those values are now required by the &lt;code&gt;--ci-parallel-node-total&lt;/code&gt; and &lt;code&gt;--ci-parallel-node-index&lt;/code&gt; option defaults.&lt;/p&gt;
&lt;p&gt;Help text is also updated: the &lt;code&gt;:memory:&lt;/code&gt; restriction on &lt;code&gt;--sqlite&lt;/code&gt; is removed (resolved by &lt;a href="https://www.drupal.org/node/3515347"&gt;#3515347&lt;/a&gt;), and references to &lt;code&gt;@group annotations&lt;/code&gt; are replaced with &lt;code&gt;#[Group()] attributes&lt;/code&gt; (see &lt;a href="https://www.drupal.org/node/3556580"&gt;#3556580&lt;/a&gt;). The issue was explicitly blocked on &lt;a href="https://www.drupal.org/node/3515347"&gt;#3515347&lt;/a&gt; landing first, because that change removed the internal &lt;code&gt;--test-id&lt;/code&gt; and &lt;code&gt;--execute-test&lt;/code&gt; options and simplified the arguments this MR had to cover.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;If your CI scripts pass &lt;code&gt;--php&lt;/code&gt; to &lt;code&gt;run-tests.sh&lt;/code&gt;, that argument is now a no-op and can safely be removed:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;# Before
php ./core/scripts/run-tests.sh --php &amp;quot;$(which php)&amp;quot; --url http://example.com/ --all

# After
php ./core/scripts/run-tests.sh --url http://example.com/ --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your CI job uses &lt;code&gt;--ci-parallel-node-total&lt;/code&gt; or &lt;code&gt;--ci-parallel-node-index&lt;/code&gt; without supplying the corresponding &lt;code&gt;CI_NODE_INDEX&lt;/code&gt; / &lt;code&gt;CI_NODE_TOTAL&lt;/code&gt; environment variables, set them explicitly (or pass the values directly on the command line) to avoid an argument-without-value error.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: July 19, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (8 months later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;jonathan1055&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The issue was blocked for several months waiting on &lt;a href="https://www.drupal.org/node/3515347"&gt;#3515347&lt;/a&gt;. Once that landed, jonathan1055 unblocked it and tested the MR in a real contrib CI pipeline using the Scheduler project's test suite, sharing pipeline links and comparing logs with the non-patched version. This surfaced a concrete bug: the startup banner was printing &lt;code&gt;/usr/bin/sudo&lt;/code&gt; as the PHP binary path instead of the actual PHP executable. mondrake traced the root cause to the old environment-variable-sniffing logic and replaced it with &lt;code&gt;PhpExecutableFinder::find()&lt;/code&gt; from &lt;code&gt;symfony/process&lt;/code&gt;. jonathan1055 then retested and confirmed the fix. He also manually tested &lt;code&gt;--list&lt;/code&gt;, &lt;code&gt;--list-files&lt;/code&gt;, &lt;code&gt;--help&lt;/code&gt;, and &lt;code&gt;--class&lt;/code&gt;, which prompted clarifications in the help text around fully-qualified class names and escaped backslashes on the command line.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3562645: Fix return types and baselined errors of core/tests/ Build|FunctionalJavascript|Functional code - round 4</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3562645.md</link>
      <guid isPermaLink="false">019d8885-2896-707b-a265-4015740701af</guid>
      <pubDate>Mon, 13 Apr 2026 19:25:14 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; main&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/62b09c02ca15f8a8885468359a6316d87cac6714"&gt;62b09c0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3562645"&gt;#3562645&lt;/a&gt; · 5 contributors · 17 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is a purely internal code-quality change that adds missing PHP return type declarations to test helper classes and traits in Drupal core's Build, Functional, and FunctionalJavascript test suites. It has no effect on production sites or end users.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Part of a multi-round effort to bring &lt;code&gt;core/tests/&lt;/code&gt; up to full static-analysis coverage (see also &lt;a href="https://www.drupal.org/node/3551596"&gt;#3551596&lt;/a&gt; round 1, &lt;a href="https://www.drupal.org/node/3562361"&gt;#3562361&lt;/a&gt; round 2, &lt;a href="https://www.drupal.org/node/3562868"&gt;#3562868&lt;/a&gt; round 3, and &lt;a href="https://www.drupal.org/node/3579189"&gt;#3579189&lt;/a&gt; round 5 for Kernel tests). Round 4 targeted the Build, Functional, and FunctionalJavascript test namespaces, manually adding return types that Rector could not add automatically.&lt;/p&gt;
&lt;p&gt;The commit (62b09c02) touches 26 files across three areas:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build tests&lt;/strong&gt; (&lt;code&gt;core/tests/Drupal/BuildTests/Framework/BuildTestBase.php&lt;/code&gt;): Added &lt;code&gt;void&lt;/code&gt; to &lt;code&gt;standUpServer()&lt;/code&gt;, &lt;code&gt;stopServer()&lt;/code&gt;; &lt;code&gt;?string&lt;/code&gt; parameter type and &lt;code&gt;void&lt;/code&gt; return to the overloaded &lt;code&gt;working_dir&lt;/code&gt; parameters in &lt;code&gt;instantiateServer()&lt;/code&gt; and &lt;code&gt;copyCodebase()&lt;/code&gt;; &lt;code&gt;string&lt;/code&gt; return to &lt;code&gt;getWorkingPath()&lt;/code&gt;, &lt;code&gt;getWorkspaceDirectory()&lt;/code&gt;, &lt;code&gt;getDrupalRoot()&lt;/code&gt;, &lt;code&gt;getDrupalRootStatic()&lt;/code&gt;; and &lt;code&gt;Finder&lt;/code&gt; return to &lt;code&gt;getCodebaseFinder()&lt;/code&gt;. The subclass &lt;code&gt;TemplateProjectTestBase&lt;/code&gt; in &lt;code&gt;package_manager&lt;/code&gt; tests received the same typed override for &lt;code&gt;instantiateServer()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FunctionalJavascript tests&lt;/strong&gt;: &lt;code&gt;WebDriverTestBase&lt;/code&gt; gained &lt;code&gt;Session&lt;/code&gt; return on &lt;code&gt;initMink()&lt;/code&gt; (requiring a new &lt;code&gt;use Behat\Mink\Session&lt;/code&gt; import), &lt;code&gt;void&lt;/code&gt; on &lt;code&gt;installModulesFromClassProperty()&lt;/code&gt;, &lt;code&gt;initFrontPage()&lt;/code&gt;, &lt;code&gt;assertJsCondition()&lt;/code&gt;, and &lt;code&gt;createScreenshot()&lt;/code&gt;. &lt;code&gt;JSWebAssert&lt;/code&gt; had &lt;code&gt;?NodeElement&lt;/code&gt; return added to &lt;code&gt;waitForButton()&lt;/code&gt;, &lt;code&gt;waitForLink()&lt;/code&gt;, &lt;code&gt;waitForField()&lt;/code&gt;, &lt;code&gt;waitForId()&lt;/code&gt;, and &lt;code&gt;waitOnAutocomplete()&lt;/code&gt;. &lt;code&gt;PerformanceTestBase&lt;/code&gt; received &lt;code&gt;void&lt;/code&gt; on &lt;code&gt;prepareEnvironment()&lt;/code&gt; and &lt;code&gt;installModulesFromClassProperty()&lt;/code&gt;. &lt;code&gt;AjaxFormPageCacheTest::getFormBuildId()&lt;/code&gt; gained a &lt;code&gt;string&lt;/code&gt; return type.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Functional tests&lt;/strong&gt;: &lt;code&gt;InstallerTestBase&lt;/code&gt; received &lt;code&gt;void&lt;/code&gt; on all installer lifecycle methods (&lt;code&gt;visitInstaller()&lt;/code&gt;, &lt;code&gt;setUpLanguage()&lt;/code&gt;, &lt;code&gt;setUpProfile()&lt;/code&gt;, &lt;code&gt;setUpSettings()&lt;/code&gt;, &lt;code&gt;setUpRequirementsProblem()&lt;/code&gt;, &lt;code&gt;setUpSite()&lt;/code&gt;, &lt;code&gt;refreshVariables()&lt;/code&gt;, &lt;code&gt;initFrontPage()&lt;/code&gt;). Subclasses &lt;code&gt;InstallerExistingConfigSyncDirectoryMultilingualTest&lt;/code&gt; and &lt;code&gt;InstallerSiteConfigProfileTest&lt;/code&gt; had their &lt;code&gt;setUpProfile()&lt;/code&gt; / &lt;code&gt;setUpSite()&lt;/code&gt; overrides fixed to drop the now-incorrect &lt;code&gt;return parent::...&lt;/code&gt; statement since the parent now returns &lt;code&gt;void&lt;/code&gt;. &lt;code&gt;UpdatePathTestBase&lt;/code&gt; had &lt;code&gt;void&lt;/code&gt; added to all of its methods including the abstract &lt;code&gt;setDatabaseDumpFiles(): void&lt;/code&gt;. Several REST resource test bases (&lt;code&gt;BaseFieldOverrideResourceTestBase&lt;/code&gt;, &lt;code&gt;DateFormatResourceTestBase&lt;/code&gt;, etc.) had &lt;code&gt;void&lt;/code&gt; added to &lt;code&gt;setUpAuthorization()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared trait&lt;/strong&gt;: &lt;code&gt;CronRunTrait::cronRun()&lt;/code&gt; in &lt;code&gt;core/tests/Drupal/Tests/Traits/Core/CronRunTrait.php&lt;/code&gt; gained a &lt;code&gt;void&lt;/code&gt; return type, eliminating many &lt;code&gt;missingType.return&lt;/code&gt; baseline entries across the modules that use this trait (block, field, help, locale, search, system, update).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Variable initialisation&lt;/strong&gt;: &lt;code&gt;ComposerProjectTemplatesTest&lt;/code&gt; had an undefined variable &lt;code&gt;$project_stabilities&lt;/code&gt; fixed by adding &lt;code&gt;$project_stabilities = []&lt;/code&gt; before its first use, resolving a &lt;code&gt;variable.undefined&lt;/code&gt; PHPStan error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Baseline cleanup&lt;/strong&gt;: All resolved errors were removed from &lt;code&gt;core/.phpstan-baseline.php&lt;/code&gt; (316 lines removed, 62 lines added), shrinking the suppression list by roughly 48 entries of type &lt;code&gt;missingType.return&lt;/code&gt; and one &lt;code&gt;variable.undefined&lt;/code&gt;. Some entries remain because they originate in runtime traits that cannot be fixed in this issue.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: December 11, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (4 months later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;dcam&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Reviewer dcam verified all new type hints, found nothing to flag, and cleared the issue for RTBC after asking only for a rebase of the baseline file due to merge conflicts. mondrake subsequently adjusted the baseline and self-RTBCed per dcam's explicit permission.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3571400: Deprecate functions in menu_ui.module and move to hooks or helper class</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3571400.md</link>
      <guid isPermaLink="false">019d87b0-d2ce-74cd-a821-aeafe2503ce9</guid>
      <category>Module maintainers</category>
      <pubDate>Mon, 13 Apr 2026 16:04:08 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/41ea6777c4e09e20c34835af1f5f99134ef64366"&gt;41ea677&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3571400"&gt;#3571400&lt;/a&gt; · 6 contributors · 39 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Six procedural functions in &lt;code&gt;menu_ui.module&lt;/code&gt; that handled node menu link saving and form building have been deprecated in Drupal 11.4.0. Their logic has been moved to two proper OOP homes: a new &lt;code&gt;MenuUiUtility&lt;/code&gt; service class, and new methods on the existing &lt;code&gt;MenuUiHooks&lt;/code&gt; class. Site owners and module developers who call these functions directly will see deprecation notices and should update to the new equivalents before Drupal 12.0.0 or 13.0.0.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; Six procedural functions in &lt;code&gt;menu_ui.module&lt;/code&gt; deprecated 11.4.0: &lt;code&gt;_menu_ui_node_save()&lt;/code&gt; and form handlers moved to &lt;code&gt;MenuUiHooks&lt;/code&gt;; &lt;code&gt;menu_ui_get_menu_link_defaults()&lt;/code&gt; moved to &lt;code&gt;MenuUiUtility::getMenuLinkDefaults()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;MenuUiUtility&lt;/code&gt; service class with public &lt;code&gt;getMenuLinkDefaults(NodeInterface $node)&lt;/code&gt; method and &lt;code&gt;@internal&lt;/code&gt; &lt;code&gt;menuUiNodeSave()&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The six procedural functions that lived in &lt;code&gt;core/modules/menu_ui/menu_ui.module&lt;/code&gt; were split across two destinations based on their nature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New &lt;code&gt;MenuUiUtility&lt;/code&gt; service&lt;/strong&gt; (&lt;code&gt;core/modules/menu_ui/src/MenuUiUtility.php&lt;/code&gt;): &lt;code&gt;_menu_ui_node_save()&lt;/code&gt; and &lt;code&gt;menu_ui_get_menu_link_defaults()&lt;/code&gt; were moved here as &lt;code&gt;menuUiNodeSave()&lt;/code&gt; and &lt;code&gt;getMenuLinkDefaults()&lt;/code&gt; respectively. These functions carried substantive logic used outside the hook system (e.g., in &lt;code&gt;MenuSettingsConstraintValidator&lt;/code&gt; and form submit handlers), making a standalone service appropriate. The service is autowired and registered in &lt;code&gt;menu_ui.services.yml&lt;/code&gt; as &lt;code&gt;Drupal\menu_ui\MenuUiUtility: ~&lt;/code&gt;. &lt;code&gt;menuUiNodeSave()&lt;/code&gt; is annotated &lt;code&gt;@internal&lt;/code&gt;; &lt;code&gt;getMenuLinkDefaults()&lt;/code&gt; is part of the public API. In &lt;code&gt;MenuUiUtility&lt;/code&gt;, static &lt;code&gt;\Drupal::entityQuery()&lt;/code&gt; and &lt;code&gt;MenuLinkContent::create()&lt;/code&gt; calls were converted to injected &lt;code&gt;EntityTypeManagerInterface&lt;/code&gt;, &lt;code&gt;EntityRepositoryInterface&lt;/code&gt;, and &lt;code&gt;EntityFieldManagerInterface&lt;/code&gt; dependencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;MenuUiHooks&lt;/code&gt; class&lt;/strong&gt; (&lt;code&gt;core/modules/menu_ui/src/Hook/MenuUiHooks.php&lt;/code&gt;): The four form-handler procedurals (&lt;code&gt;menu_ui_node_builder&lt;/code&gt;, &lt;code&gt;menu_ui_form_node_form_submit&lt;/code&gt;, &lt;code&gt;menu_ui_form_node_type_form_validate&lt;/code&gt;, &lt;code&gt;menu_ui_form_node_type_form_builder&lt;/code&gt;) were moved in as &lt;code&gt;nodeBuilder()&lt;/code&gt;, &lt;code&gt;formNodeFormSubmit()&lt;/code&gt;, &lt;code&gt;formNodeTypeFormValidate()&lt;/code&gt;, and &lt;code&gt;formNodeTypeFormBuilder()&lt;/code&gt;. The class gained &lt;code&gt;EntityRepositoryInterface&lt;/code&gt; DI, and &lt;code&gt;MenuUiUtility&lt;/code&gt; is injected via &lt;code&gt;#[AutowireServiceClosure(MenuUiUtility::class)]&lt;/code&gt; to avoid loading the utility eagerly. Form references to old procedural callbacks (&lt;code&gt;$form['#submit'][]&lt;/code&gt;, &lt;code&gt;$form['#entity_builders'][]&lt;/code&gt;) were updated from string function names to &lt;code&gt;static::class . ':methodName'&lt;/code&gt; format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;MenuSettingsConstraintValidator&lt;/code&gt;&lt;/strong&gt; was updated to implement &lt;code&gt;ContainerInjectionInterface&lt;/code&gt; and use &lt;code&gt;AutowireTrait&lt;/code&gt;, receiving &lt;code&gt;MenuUiUtility&lt;/code&gt; via constructor injection and replacing a direct &lt;code&gt;menu_ui_get_menu_link_defaults()&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deprecation versions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_menu_ui_node_save()&lt;/code&gt; and &lt;code&gt;menu_ui_form_node_form_submit()&lt;/code&gt;, &lt;code&gt;menu_ui_form_node_type_form_validate()&lt;/code&gt;, &lt;code&gt;menu_ui_form_node_type_form_builder()&lt;/code&gt;: deprecated 11.4.0, removed 12.0.0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;menu_ui_get_menu_link_defaults()&lt;/code&gt; and &lt;code&gt;menu_ui_node_builder()&lt;/code&gt;: deprecated 11.4.0, removed 13.0.0.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All deprecated functions now emit &lt;code&gt;E_USER_DEPRECATED&lt;/code&gt; via &lt;code&gt;@trigger_error()&lt;/code&gt; and delegate to the new service methods. Change records are consolidated at https://www.drupal.org/node/3566774.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Replace direct calls to the deprecated procedural functions with service calls or class method calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// BEFORE
_menu_ui_node_save($node, $values);

// AFTER
\Drupal::service(\Drupal\menu_ui\MenuUiUtility::class)-&amp;gt;menuUiNodeSave($node, $values);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// BEFORE
$defaults = menu_ui_get_menu_link_defaults($node);

// AFTER
$defaults = \Drupal::service(\Drupal\menu_ui\MenuUiUtility::class)-&amp;gt;getMenuLinkDefaults($node);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// BEFORE (form #submit / #entity_builders references)
$form['actions'][$action]['#submit'][] = 'menu_ui_form_node_form_submit';
$form['#entity_builders'][] = 'menu_ui_node_builder';
$form['#validate'][] = 'menu_ui_form_node_type_form_validate';
$form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';

// AFTER — these are now handled internally by MenuUiHooks; if you are calling
// them directly, use the service:
\Drupal::service(\Drupal\menu_ui\Hook\MenuUiHooks::class)-&amp;gt;formNodeFormSubmit($form, $form_state);
\Drupal::service(\Drupal\menu_ui\Hook\MenuUiHooks::class)-&amp;gt;nodeBuilder($entity_type, $entity, $form, $form_state);
\Drupal::service(\Drupal\menu_ui\Hook\MenuUiHooks::class)-&amp;gt;formNodeTypeFormValidate($form, $form_state);
\Drupal::service(\Drupal\menu_ui\Hook\MenuUiHooks::class)-&amp;gt;formNodeTypeFormBuilder($entity_type, $type, $form, $form_state);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: &lt;code&gt;MenuUiUtility::menuUiNodeSave()&lt;/code&gt; is &lt;code&gt;@internal&lt;/code&gt; and not intended for direct external use.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: February 4, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (2 months and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;smustgrave (Mobomo)&lt;/li&gt;
&lt;li&gt;dcam&lt;/li&gt;
&lt;li&gt;nicxvan (nLightened Development LLC)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;claudiu.cristea (Webikon)&lt;/li&gt;
&lt;li&gt;godotislate&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;A debate in the thread shaped the final design. After smustgrave initially moved &lt;code&gt;_menu_ui_node_save&lt;/code&gt; and &lt;code&gt;menu_ui_get_menu_link_defaults&lt;/code&gt; to a utility class, nicxvan and berdir discussed whether the helpers warranted a dedicated service at all, since they are form-related rather than a general API. berdir noted the functions are &amp;quot;form related callbacks and helpers, not an API,&amp;quot; and that DI was &amp;quot;always tricky with these issues.&amp;quot; godotislate later reviewed the MR and expressed reluctance about the utility class (&amp;quot;rickety menu UI code lives on as service code&amp;quot;), preferring the logic be moved or marked &lt;code&gt;@internal&lt;/code&gt; to avoid expanding the public API surface. nicxvan argued for an internal utility class so it stays isolated from the hook service. That compromise held: &lt;code&gt;menuUiNodeSave()&lt;/code&gt; was marked &lt;code&gt;@internal&lt;/code&gt; while &lt;code&gt;getMenuLinkDefaults()&lt;/code&gt; remained public. At commit time godotislate applied two final refinements directly: moving the &lt;code&gt;@internal&lt;/code&gt; annotation from the class level down to just the &lt;code&gt;menuUiNodeSave()&lt;/code&gt; method, and switching the service declaration to autowire form &lt;code&gt;Drupal\menu_ui\MenuUiUtility: ~&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3581958: Alter hook for site configure form in DemoUmamiHooks uses outdated services</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3581958.md</link>
      <guid isPermaLink="false">019d87b2-706c-70fa-903a-0caa7a8793cd</guid>
      <pubDate>Mon, 13 Apr 2026 15:12:43 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/1bf6e1fd033c73478b352f58d473012d97f979ff"&gt;1bf6e1f&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3581958"&gt;#3581958&lt;/a&gt; · 5 contributors · 23 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 5&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Installing the Umami demo profile through the browser UI with &amp;quot;Check for updates automatically&amp;quot; enabled caused a fatal error after submitting the site configuration form. The page displayed &amp;quot;The website encountered an unexpected error&amp;quot; with a Symfony DependencyInjection RuntimeException about a synthetic service. This bug has been fixed; Umami can now be installed through the UI without errors regardless of whether the update status module is enabled.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Umami install profile's &lt;code&gt;DemoUmamiHooks&lt;/code&gt; class implements &lt;code&gt;hook_form_install_configure_form_alter&lt;/code&gt; (via the &lt;code&gt;#[Hook]&lt;/code&gt; attribute) to add a submit handler to the site configuration form. When the form is submitted with &amp;quot;Check for updates automatically&amp;quot; checked, &lt;code&gt;SiteConfigureForm::submitForm()&lt;/code&gt; installs the &lt;code&gt;update&lt;/code&gt; module, which triggers a full container rebuild. Any service objects instantiated from the old container become stale.&lt;/p&gt;
&lt;p&gt;The submit handler was registered as the instance callable &lt;code&gt;[$this, 'installConfigureSubmit']&lt;/code&gt;, where &lt;code&gt;$this&lt;/code&gt; is the &lt;code&gt;DemoUmamiHooks&lt;/code&gt; service object from the old container. When Drupal invokes this handler after the container rebuild, &lt;code&gt;installConfigureSubmit()&lt;/code&gt; calls &lt;code&gt;setUserPasswords()&lt;/code&gt;, which used &lt;code&gt;$this-&amp;gt;entityTypeManager-&amp;gt;getStorage('user')&lt;/code&gt; -- the &lt;code&gt;entityTypeManager&lt;/code&gt; was injected from the old container and is now stale, causing the crash.&lt;/p&gt;
&lt;p&gt;The fix converts both &lt;code&gt;installConfigureSubmit()&lt;/code&gt; and &lt;code&gt;setUserPasswords()&lt;/code&gt; to &lt;code&gt;static&lt;/code&gt; methods in &lt;code&gt;DemoUmamiHooks&lt;/code&gt;. The callable in &lt;code&gt;formInstallConfigureFormAlter()&lt;/code&gt; is changed from &lt;code&gt;[$this, 'installConfigureSubmit']&lt;/code&gt; to &lt;code&gt;[static::class, 'installConfigureSubmit']&lt;/code&gt;, and &lt;code&gt;setUserPasswords()&lt;/code&gt; replaces &lt;code&gt;$this-&amp;gt;entityTypeManager-&amp;gt;getStorage('user')&lt;/code&gt; with &lt;code&gt;\Drupal::entityTypeManager()-&amp;gt;getStorage('user')&lt;/code&gt;. Using the global &lt;code&gt;\Drupal&lt;/code&gt; static ensures the entity type manager is always retrieved from the current (rebuilt) container. The &lt;code&gt;EntityTypeManagerInterface&lt;/code&gt; constructor parameter is removed from &lt;code&gt;DemoUmamiHooks&lt;/code&gt; entirely. A code comment was added to &lt;code&gt;setUserPasswords()&lt;/code&gt; explaining why DI is intentionally avoided here. Both methods are also marked &lt;code&gt;@internal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The root cause was traced (via &lt;code&gt;git bisect&lt;/code&gt;) to &lt;a href="https://www.drupal.org/node/3516264"&gt;#3516264&lt;/a&gt;, where &lt;code&gt;CKEditor5Hooks&lt;/code&gt; was refactored to inject &lt;code&gt;ModuleHandlerInterface&lt;/code&gt; and &lt;code&gt;LibraryDependencyResolverInterface&lt;/code&gt; via DI instead of calling &lt;code&gt;\Drupal::moduleHandler()&lt;/code&gt; inline -- the opposite pattern. The sibling fix for &lt;code&gt;SiteConfigureForm&lt;/code&gt; itself is &lt;a href="https://www.drupal.org/node/3573856"&gt;#3573856&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Test coverage was added by overriding &lt;code&gt;installParameters()&lt;/code&gt; in &lt;code&gt;UmamiMultilingualInstallTest&lt;/code&gt; to set &lt;code&gt;enable_update_status_module = TRUE&lt;/code&gt;, so the functional test now exercises the container-rebuild path.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 27, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (16 days and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;godotislate&lt;/li&gt;
&lt;li&gt;benjifisher (Harvard Web Publishing)&lt;/li&gt;
&lt;li&gt;nicxvan (nLightened Development LLC)&lt;/li&gt;
&lt;li&gt;dries&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;benjifisher's review drove several improvements. After reproducing the error manually on both &lt;code&gt;main&lt;/code&gt; and the feature branch, he used &lt;code&gt;git bisect&lt;/code&gt; to identify &lt;a href="https://www.drupal.org/node/3516264"&gt;#3516264&lt;/a&gt; as the change that introduced the regression, and explained the link between that issue's DI refactor and the crash. He requested a code comment in &lt;code&gt;setUserPasswords()&lt;/code&gt; explaining why DI is not used, which godotislate added. He also opened a broader discussion about guidelines for using DI safely around container rebuilds, to which godotislate responded with a clear explanation of the mechanism: a container rebuild invalidates all service objects instantiated from the previous container, and static methods with &lt;code&gt;\Drupal::&lt;/code&gt; calls are the correct escape hatch in this situation. dries spotted a minor inaccuracy in the added comment (&lt;code&gt;SiteConfigureForm::submit()&lt;/code&gt; vs. &lt;code&gt;SiteConfigureForm::submitForm()&lt;/code&gt;), which was also corrected before the issue was marked RTBC.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3581407: Remove unused properties from unit tests</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3581407.md</link>
      <guid isPermaLink="false">019d870b-7349-71f6-87b0-dda07a7c8e36</guid>
      <pubDate>Mon, 13 Apr 2026 08:07:14 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; main&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/473f5e49924f158ba101b2ae4ca2f479ab90d017"&gt;473f5e4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3581407"&gt;#3581407&lt;/a&gt; · 4 contributors · 15 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Thirteen unused protected properties were removed from unit test classes across Drupal core. The properties were declared at the class level but never actually read anywhere in the tests, making them dead code. The cleanup has no effect on test coverage or runtime behavior.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;This issue was discovered via &lt;code&gt;shipmonk/dead-code-detector&lt;/code&gt; (tracked in &lt;a href="https://www.drupal.org/node/3581155"&gt;#3581155&lt;/a&gt;). The single commit removes 109 lines of dead property declarations from 13 unit test files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BlockPageVariantTest&lt;/code&gt; - &lt;code&gt;$contextHandler&lt;/code&gt; (&lt;code&gt;ContextHandlerInterface&lt;/code&gt; mock)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FieldStorageConfigEntityUnitTest&lt;/code&gt; - &lt;code&gt;$entityTypeId&lt;/code&gt; (string)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FieldUninstallValidatorTest&lt;/code&gt; - &lt;code&gt;$fieldTypePluginManager&lt;/code&gt; (&lt;code&gt;FieldTypePluginManagerInterface&lt;/code&gt; mock)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FormErrorHandlerTest&lt;/code&gt; - &lt;code&gt;$linkGenerator&lt;/code&gt; (&lt;code&gt;LinkGeneratorInterface&lt;/code&gt; mock)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContentLanguageSettingsUnitTest&lt;/code&gt; - &lt;code&gt;$entityTypeId&lt;/code&gt; (string, plus its &lt;code&gt;setUp()&lt;/code&gt; assignment)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MigrateSourceTest&lt;/code&gt; - &lt;code&gt;$sourceIds&lt;/code&gt; (array)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SubProcessTest&lt;/code&gt; - &lt;code&gt;$plugin&lt;/code&gt; (&lt;code&gt;SubProcess&lt;/code&gt; instance)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AliasManagerTest&lt;/code&gt; - &lt;code&gt;$cacheKey&lt;/code&gt; and &lt;code&gt;$path&lt;/code&gt; (both strings)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SearchPluginCollectionTest&lt;/code&gt; - &lt;code&gt;$pluginInstances&lt;/code&gt; (array of &lt;code&gt;SearchInterface&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TimestampItemNormalizerTest&lt;/code&gt; - &lt;code&gt;$item&lt;/code&gt; (&lt;code&gt;TimestampItem&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PathBasedBreadcrumbBuilderTest&lt;/code&gt; - &lt;code&gt;$linkGenerator&lt;/code&gt; property and a corresponding &lt;code&gt;setLinkGenerator()&lt;/code&gt; setter on the &lt;code&gt;TestPathBasedBreadcrumbBuilder&lt;/code&gt; stub class, plus the &lt;code&gt;use&lt;/code&gt; import for &lt;code&gt;LinkGeneratorInterface&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserRegistrationResourceTest&lt;/code&gt; - &lt;code&gt;ERROR_MESSAGE&lt;/code&gt; constant and &lt;code&gt;$reflection&lt;/code&gt; (&lt;code&gt;ReflectionClass&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessagesTest&lt;/code&gt; (views) - &lt;code&gt;$view&lt;/code&gt; (&lt;code&gt;ViewExecutable&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The investigation in comments traced several of these properties back to their original additions (e.g. &lt;code&gt;ContentLanguageSettingsUnitTest&lt;/code&gt; to &lt;a href="https://www.drupal.org/node/2355909"&gt;#2355909&lt;/a&gt;, &lt;code&gt;SubProcessTest&lt;/code&gt; to &lt;a href="https://www.drupal.org/node/2147811"&gt;#2147811&lt;/a&gt;, &lt;code&gt;AliasManagerTest&lt;/code&gt; to &lt;a href="https://www.drupal.org/node/2233623"&gt;#2233623&lt;/a&gt;) and confirmed they were never read from the start. A companion issue (&lt;a href="https://www.drupal.org/node/3581404"&gt;#3581404&lt;/a&gt;) handled the same cleanup for kernel tests. The scope was intentionally limited to properties flagged by &lt;code&gt;shipmonk/dead-code-detector&lt;/code&gt;; any additional properties spotted during review were noted for separate follow-up issues.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 25, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (18 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;borisson_ (Calibrate)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;li&gt;smustgrave (Mobomo)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#2986699: Add missing getter method to retrieve range (limit/offset) from Select query objects</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/2986699.md</link>
      <guid isPermaLink="false">019d8703-f523-76fa-bb44-ffd3bf584446</guid>
      <category>Module maintainers</category>
      <pubDate>Mon, 13 Apr 2026 07:49:34 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/b124145238c7dfaf2ba4d177784b03d0550675f5"&gt;b124145&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/2986699"&gt;#2986699&lt;/a&gt; · 9 contributors · 35 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 10&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Drupal's &lt;code&gt;Select&lt;/code&gt; query builder has always supported &lt;code&gt;$query-&amp;gt;range($start, $length)&lt;/code&gt; to apply LIMIT/OFFSET, but there was no public way to read that range back out of the query object. This made it impossible for &lt;code&gt;hook_query_alter()&lt;/code&gt; implementations, query decorators, and contrib modules to inspect whether a range had already been set and what its values were. A new &lt;code&gt;getRange()&lt;/code&gt; method is now available on all &lt;code&gt;Select&lt;/code&gt; query objects, returning &lt;code&gt;null&lt;/code&gt; when no range has been configured, or an associative array with &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;length&lt;/code&gt; keys when one has. The change is backward compatible.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;getRange(): ?array&lt;/code&gt; method on &lt;code&gt;SelectInterface&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, and &lt;code&gt;SelectExtender&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; when no range is set, or &lt;code&gt;['start' =&amp;gt; int, 'length' =&amp;gt; ?int]&lt;/code&gt; when one is.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;Select&lt;/code&gt; class stores its range limiter in a &lt;code&gt;protected $range&lt;/code&gt; property (typed &lt;code&gt;array|null&lt;/code&gt;). Before this change the property could only be written via &lt;code&gt;range()&lt;/code&gt; and was not readable through any public API.&lt;/p&gt;
&lt;p&gt;The fix adds &lt;code&gt;getRange(): ?array&lt;/code&gt; to three places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SelectInterface&lt;/code&gt;&lt;/strong&gt; (in &lt;code&gt;core/lib/Drupal/Core/Database/Query/SelectInterface.php&lt;/code&gt;): new method declaration with a precise phpdoc return type of &lt;code&gt;array{start: int, length: ?int}|null&lt;/code&gt; and descriptions of the two array keys (&lt;code&gt;start&lt;/code&gt; — first record to return; &lt;code&gt;length&lt;/code&gt; — number of records to return).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Select&lt;/code&gt;&lt;/strong&gt; (in &lt;code&gt;core/lib/Drupal/Core/Database/Query/Select.php&lt;/code&gt;): concrete implementation that returns &lt;code&gt;$this-&amp;gt;range&lt;/code&gt; by reference, consistent with the existing by-reference getters (&lt;code&gt;getTables()&lt;/code&gt;, &lt;code&gt;getGroupBy()&lt;/code&gt;). The &lt;code&gt;@var array&lt;/code&gt; docblock on &lt;code&gt;$range&lt;/code&gt; was also corrected to &lt;code&gt;@var array|null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SelectExtender&lt;/code&gt;&lt;/strong&gt; (in &lt;code&gt;core/lib/Drupal/Core/Database/Query/SelectExtender.php&lt;/code&gt;): delegation wrapper that forwards the call to &lt;code&gt;$this-&amp;gt;query-&amp;gt;getRange()&lt;/code&gt;, also by reference.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The method returns by reference so that callers can mutate the range in-place (as demonstrated by the new kernel tests). Returning &lt;code&gt;null&lt;/code&gt; — rather than an empty array — when no range is set was a deliberate decision made during review; &lt;code&gt;drunken monkey&lt;/code&gt; pointed out the discrepancy between an earlier draft (which returned &lt;code&gt;[]&lt;/code&gt;) and the issue summary's stated intent of returning &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;New kernel test class &lt;code&gt;SelectGetRangeTest&lt;/code&gt; (in &lt;code&gt;core/tests/Drupal/KernelTests/Core/Database/SelectGetRangeTest.php&lt;/code&gt;) covers both &lt;code&gt;Select&lt;/code&gt; and &lt;code&gt;SelectExtender&lt;/code&gt;: it asserts &lt;code&gt;null&lt;/code&gt; before any range is applied, correct array values after &lt;code&gt;range()&lt;/code&gt; is called, and that the returned reference is live (mutations via the reference affect the query).&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: July 18, 2018&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (7 years and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;borisson_ (Calibrate)&lt;/li&gt;
&lt;li&gt;drunken monkey&lt;/li&gt;
&lt;li&gt;neptune-dc&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The issue was originally filed by drunken monkey in July 2018 with a patch, but it stalled without a test. Seven years later, at a Bug Smash Initiative session, luke.stewart picked it up, verified the feature was still missing, and noted it needed tests and possibly a change record. neptune-dc opened a merge request with kernel tests. drunken monkey returned to review, added a return type hint to the interface declaration, and flagged the mismatch between the draft returning &lt;code&gt;[]&lt;/code&gt; and the stated intent of returning &lt;code&gt;null&lt;/code&gt; — neptune-dc updated the code accordingly. smustgrave rebased and aligned the change record for the &lt;code&gt;main&lt;/code&gt; branch before catch committed it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3562868: Add types to class properties in core/tests code via Rector - round 3</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3562868.md</link>
      <guid isPermaLink="false">019d8707-a3a4-777b-8ca0-6aea9a867806</guid>
      <pubDate>Mon, 13 Apr 2026 07:35:51 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; main&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/03614b275dda27980cbdca726a783c3b0990d24a"&gt;03614b2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3562868"&gt;#3562868&lt;/a&gt; · 4 contributors · 20 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is a purely internal code-quality change that adds native PHP type declarations to class properties in Drupal's &lt;code&gt;core/tests&lt;/code&gt; directory. It has no effect on site behavior, configuration, or the public API. Because the changes are confined to test code, there are no backwards-compatibility concerns.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;This issue is the third in a series (following &lt;a href="https://www.drupal.org/node/3551596"&gt;#3551596&lt;/a&gt; and &lt;a href="https://www.drupal.org/node/3562361"&gt;#3562361&lt;/a&gt;) that progressively modernizes &lt;code&gt;core/tests&lt;/code&gt; code using Rector. The first two rounds added return types and parameter type hints; this round specifically targets class property declarations.&lt;/p&gt;
&lt;p&gt;The workflow ran Rector with &lt;code&gt;-&amp;gt;withPreparedSets(typeDeclarations: true)&lt;/code&gt; over &lt;code&gt;core/tests&lt;/code&gt;, then applied PHPCS to fix code style. No PHPStan baseline update was required because native property types are only enforced at PHPStan level 6; core currently runs at level 1.&lt;/p&gt;
&lt;p&gt;The commit touches around 70 test files with 618 additions and 973 deletions. The dominant pattern is replacing a &lt;code&gt;@var&lt;/code&gt; docblock with a native typed property declaration. For example, in &lt;code&gt;BuildTestBase.php&lt;/code&gt;: &lt;code&gt;private $workspaceDir&lt;/code&gt; (with &lt;code&gt;@var string&lt;/code&gt; docblock) becomes &lt;code&gt;private string $workspaceDir&lt;/code&gt;, and nullable properties like &lt;code&gt;$hostPort&lt;/code&gt; and &lt;code&gt;$commandProcess&lt;/code&gt; become &lt;code&gt;private ?int $hostPort = NULL&lt;/code&gt; and &lt;code&gt;private ?Process $commandProcess = NULL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rector cannot automatically determine intersection types for PHPUnit mock objects because it only sees the interface being mocked, not the &lt;code&gt;MockObject&lt;/code&gt; or &lt;code&gt;Stub&lt;/code&gt; wrapper. Those properties were manually typed with PHP intersection syntax after Rector's output, e.g., &lt;code&gt;protected CsrfTokenGenerator&amp;amp;MockObject $csrfToken&lt;/code&gt;, &lt;code&gt;protected StorageInterface&amp;amp;Stub $storage&lt;/code&gt;. One pre-existing error was also corrected: &lt;code&gt;FileUrlGeneratorInterface|MockObject&lt;/code&gt; (a union type) was fixed to &lt;code&gt;FileUrlGeneratorInterface&amp;amp;MockObject&lt;/code&gt; (the correct intersection type).&lt;/p&gt;
&lt;p&gt;Where a single property is assigned both a &lt;code&gt;MockObject&lt;/code&gt; and a &lt;code&gt;Stub&lt;/code&gt; in different test methods, a &lt;code&gt;@todo split the variable to allow proper type&lt;/code&gt; annotation was added and the property was left with a plain interface type. The change is a pure mechanical transformation plus the manual intersection-type additions; mondrake explicitly avoided fixing other issues along the way to keep the MR focused.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: December 12, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (4 months later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;borisson_ noticed test failures in the merge request and asked whether something was wrong with the Rector rule. mondrake explained that the failures stemmed from missing deprecations in the calling code rather than a misconfigured rule, and noted that Rector cannot intersect MockObject types automatically, requiring manual additions. smustgrave asked whether a PHPStan baseline update was needed and whether new violations would be caught going forward; mondrake clarified that Rector and PHPStan are independent tools and that native property types only become a PHPStan concern at level 6, which core has not yet reached.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3522561: Fully type StatementInterface methods' parameters</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3522561.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0ef9060f6687</guid>
      <category>Module maintainers</category>
      <pubDate>Sun, 12 Apr 2026 20:26:14 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/ab14cc057084491a2f85a4f31066c5c4cd0e6cc9"&gt;ab14cc0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3522561"&gt;#3522561&lt;/a&gt; · 7 contributors · 38 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;All method parameters in &lt;code&gt;StatementInterface&lt;/code&gt; now carry proper PHP type declarations. Previously, the interface used untyped parameters that relied on runtime &lt;code&gt;assert()&lt;/code&gt; checks inside &lt;code&gt;StatementBase&lt;/code&gt; to validate types. Module developers who implement or extend &lt;code&gt;StatementInterface&lt;/code&gt; directly will need to update their method signatures to match the new typed versions. Additionally, the &lt;code&gt;$cursor_orientation&lt;/code&gt; and &lt;code&gt;$cursor_offset&lt;/code&gt; parameters of &lt;code&gt;fetch()&lt;/code&gt; are deprecated as of drupal:11.4.0 and will be removed in drupal:12.0.0 with no replacement.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; All &lt;code&gt;StatementInterface&lt;/code&gt; method parameters now carry PHP type declarations; &lt;code&gt;$cursor_orientation&lt;/code&gt; and &lt;code&gt;$cursor_offset&lt;/code&gt; on &lt;code&gt;fetch()&lt;/code&gt; are deprecated in drupal:11.4.0 and removed in drupal:12.0.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; &lt;code&gt;fetch()&lt;/code&gt; parameters &lt;code&gt;$cursor_orientation&lt;/code&gt; and &lt;code&gt;$cursor_offset&lt;/code&gt; deprecated in drupal:11.4.0, removed in drupal:12.0.0; no replacement.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The database layer's &lt;code&gt;StatementInterface&lt;/code&gt; (&lt;code&gt;core/lib/Drupal/Core/Database/StatementInterface.php&lt;/code&gt;) had untyped parameters across its fetch and execute methods that were only validated at runtime via &lt;code&gt;assert()&lt;/code&gt; calls in &lt;code&gt;StatementBase&lt;/code&gt;. This issue replaced those assertions with proper PHP type declarations on both the interface and its base implementation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;StatementInterface&lt;/code&gt; signature changes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;execute(?array $args = [], array $options = [])&lt;/code&gt; -- was fully untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setFetchMode(FetchAs $mode, string|int|null $a1 = NULL, array $a2 = [])&lt;/code&gt; -- &lt;code&gt;$mode&lt;/code&gt; was untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch(?FetchAs $mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL)&lt;/code&gt; -- &lt;code&gt;$mode&lt;/code&gt; was untyped; &lt;code&gt;$cursor_orientation&lt;/code&gt; and &lt;code&gt;$cursor_offset&lt;/code&gt; are now deprecated in drupal:11.4.0 (removal in drupal:12.0.0, see &lt;a href="https://www.drupal.org/node/3551924"&gt;#3551924&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchField(int $index = 0)&lt;/code&gt; -- was untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchAll(?FetchAs $mode = NULL, ?int $column_index = NULL, ?array $constructor_arguments = NULL)&lt;/code&gt; -- was untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchCol(int $index = 0)&lt;/code&gt; -- was untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchAllKeyed(int $key_index = 0, int $value_index = 1)&lt;/code&gt; -- was untyped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchAllAssoc(string $key, ?FetchAs $fetch = NULL)&lt;/code&gt; -- both parameters were untyped; &lt;code&gt;$fetch&lt;/code&gt; also drops the previously documented (and already-deprecated) &lt;code&gt;int|string&lt;/code&gt; union for PDO constants, accepting only &lt;code&gt;?FetchAs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In &lt;code&gt;StatementBase&lt;/code&gt; (&lt;code&gt;core/lib/Drupal/Core/Database/Statement/StatementBase.php&lt;/code&gt;), the &lt;code&gt;assert()&lt;/code&gt; guards were removed and the concrete signatures were updated accordingly. In the 11.x backport, &lt;code&gt;StatementBase&lt;/code&gt; retains &lt;code&gt;FetchAs|int&lt;/code&gt; union types for &lt;code&gt;$mode&lt;/code&gt; arguments on &lt;code&gt;setFetchMode()&lt;/code&gt;, &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;fetchAll()&lt;/code&gt;, and &lt;code&gt;fetchAllAssoc()&lt;/code&gt; to preserve backward compatibility with the existing deprecation path for integer &lt;code&gt;PDO::FETCH_*&lt;/code&gt; constants; the interface itself uses only &lt;code&gt;FetchAs&lt;/code&gt; since parameter types are &lt;strong&gt;contravariant&lt;/strong&gt; -- an implementing class may accept types at least as wide as the interface declares, so tightening the interface signature requires no deprecation cycle. This design insight, raised by longwave in comment #21, simplified the implementation substantially (the original approach had planned a full &lt;code&gt;@todo&lt;/code&gt;/commented-out deprecation dance, which turned out to be unnecessary for parameter types as opposed to return types).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;StatementInterface&lt;/code&gt; phpdoc example code was also updated to reference &lt;code&gt;StatementBase&lt;/code&gt; instead of &lt;code&gt;StatementWrapperIterator&lt;/code&gt; and to use fully-qualified class names. The &lt;code&gt;fetchObject()&lt;/code&gt; return type in the interface was refined from &lt;code&gt;mixed&lt;/code&gt; to &lt;code&gt;object|false|null&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Any class that directly implements &lt;code&gt;StatementInterface&lt;/code&gt; (rather than extending &lt;code&gt;StatementBase&lt;/code&gt;) must update its method signatures:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Before
public function execute($args = [], $options = []) {}
public function setFetchMode($mode, $a1 = NULL, $a2 = []) {}
public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {}
public function fetchField($index = 0) {}
public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {}
public function fetchCol($index = 0) {}
public function fetchAllKeyed($key_index = 0, $value_index = 1) {}
public function fetchAllAssoc($key, $fetch = NULL) {}

// After
public function execute(?array $args = [], array $options = []) {}
public function setFetchMode(FetchAs $mode, string|int|null $a1 = NULL, array $a2 = []) {}
public function fetch(?FetchAs $mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {}
public function fetchField(int $index = 0) {}
public function fetchAll(?FetchAs $mode = NULL, ?int $column_index = NULL, ?array $constructor_arguments = NULL) {}
public function fetchCol(int $index = 0) {}
public function fetchAllKeyed(int $key_index = 0, int $value_index = 1) {}
public function fetchAllAssoc(string $key, ?FetchAs $fetch = NULL) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any call to &lt;code&gt;fetch()&lt;/code&gt; that passes non-&lt;code&gt;NULL&lt;/code&gt; values for &lt;code&gt;$cursor_orientation&lt;/code&gt; or &lt;code&gt;$cursor_offset&lt;/code&gt; will trigger a deprecation error in drupal:11.4.0. Remove those arguments; there is no replacement.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: May 3, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 11, 2026 (11 months and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;smustgrave (Mobomo)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;dcam&lt;/li&gt;
&lt;li&gt;godotislate&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;longwave's comment #21 pointed out that PHP parameter types are contravariant -- adding a type hint to an interface parameter does not break implementing classes, because PHP allows implementors to accept a wider type. This observation made the planned deprecation scaffolding unnecessary for parameter type additions, simplifying the patch significantly. mondrake incorporated this in the next push.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3583849: Deprecate PgSql Connection::*Savepoint() methods</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3583849.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867c2f3afe10</guid>
      <category>Module maintainers</category>
      <pubDate>Sat, 11 Apr 2026 21:40:46 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/5b4ad871353d9c49e89f909b9a1b073e028d5459"&gt;5b4ad87&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583849"&gt;#3583849&lt;/a&gt; · 4 contributors · 12 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Three PostgreSQL-specific savepoint methods on the database connection class (&lt;code&gt;addSavepoint()&lt;/code&gt;, &lt;code&gt;releaseSavepoint()&lt;/code&gt;, &lt;code&gt;rollbackSavepoint()&lt;/code&gt;) are deprecated in Drupal 11.4.0 and will be removed in 13.0.0. Custom or contributed modules that call these methods on a PostgreSQL connection need to switch to the standard &lt;code&gt;TransactionManager&lt;/code&gt; API. This is an internal change with no effect on site behavior.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; &lt;code&gt;Connection::addSavepoint()&lt;/code&gt;, &lt;code&gt;Connection::releaseSavepoint()&lt;/code&gt;, &lt;code&gt;Connection::rollbackSavepoint()&lt;/code&gt;, and the &lt;code&gt;$savepoints&lt;/code&gt; property on the PgSql driver are deprecated; use &lt;code&gt;TransactionManager::startTransaction()&lt;/code&gt; with &lt;code&gt;Transaction::commitOrRelease()&lt;/code&gt;/&lt;code&gt;rollback()&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The PostgreSQL driver in Drupal uses savepoints named &lt;code&gt;mimic_implicit_commit&lt;/code&gt; to wrap individual queries (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, UPSERT) and schema introspection calls in a rollback-safe savepoint. This lets PostgreSQL mimic MySQL/SQLite behavior where a single failed query inside a transaction does not abort the entire transaction, which is important for on-demand table creation (e.g. &lt;code&gt;\Drupal\Core\Cache\DatabaseBackend&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Previously, this was managed through three custom methods on &lt;code&gt;Drupal\pgsql\Driver\Database\pgsql\Connection&lt;/code&gt;: &lt;code&gt;addSavepoint()&lt;/code&gt;, &lt;code&gt;releaseSavepoint()&lt;/code&gt;, and &lt;code&gt;rollbackSavepoint()&lt;/code&gt;, along with a &lt;code&gt;$savepoints&lt;/code&gt; property that tracked active savepoints by name. These methods were thin wrappers around &lt;code&gt;startTransaction()&lt;/code&gt; and the &lt;code&gt;Transaction&lt;/code&gt; object's own &lt;code&gt;commitOrRelease()&lt;/code&gt;/&lt;code&gt;rollback()&lt;/code&gt; methods, making them redundant after &lt;a href="https://www.drupal.org/node/3398767"&gt;#3398767&lt;/a&gt; introduced explicit commit support and &lt;a href="https://www.drupal.org/node/3406985"&gt;#3406985&lt;/a&gt; converted all core transactions to use &lt;code&gt;::commitOrRelease()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This change deprecates all three methods and the &lt;code&gt;$savepoints&lt;/code&gt; property, adding &lt;code&gt;@trigger_error()&lt;/code&gt; calls to each. All internal call sites across eight files in &lt;code&gt;core/modules/pgsql/src/Driver/Database/pgsql/&lt;/code&gt; (&lt;code&gt;Connection.php&lt;/code&gt;, &lt;code&gt;Delete.php&lt;/code&gt;, &lt;code&gt;Insert.php&lt;/code&gt;, &lt;code&gt;Schema.php&lt;/code&gt;, &lt;code&gt;Select.php&lt;/code&gt;, &lt;code&gt;Truncate.php&lt;/code&gt;, &lt;code&gt;Update.php&lt;/code&gt;, &lt;code&gt;Upsert.php&lt;/code&gt;) were migrated to the new pattern. The old pattern called &lt;code&gt;addSavepoint()&lt;/code&gt; unconditionally (the transaction check was inside the method). The new pattern explicitly checks &lt;code&gt;$this-&amp;gt;connection-&amp;gt;inTransaction()&lt;/code&gt; first, then calls &lt;code&gt;$this-&amp;gt;connection-&amp;gt;startTransaction('mimic_implicit_commit')&lt;/code&gt; and operates directly on the returned &lt;code&gt;Transaction&lt;/code&gt; object, using &lt;code&gt;$savepoint-&amp;gt;commitOrRelease()&lt;/code&gt; on success and &lt;code&gt;$savepoint-&amp;gt;rollback()&lt;/code&gt; on exception. The &lt;code&gt;Connection::query()&lt;/code&gt; method was similarly restructured, collapsing the &lt;code&gt;$wrap_with_savepoint&lt;/code&gt; variable into a single &lt;code&gt;if&lt;/code&gt; block.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;If custom or contributed code calls &lt;code&gt;addSavepoint()&lt;/code&gt;, &lt;code&gt;releaseSavepoint()&lt;/code&gt;, or &lt;code&gt;rollbackSavepoint()&lt;/code&gt; on the PostgreSQL connection, replace them with the &lt;code&gt;TransactionManager&lt;/code&gt; pattern:&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;$this-&amp;gt;connection-&amp;gt;addSavepoint();
try {
  $result = do_something();
  $this-&amp;gt;connection-&amp;gt;releaseSavepoint();
}
catch (\Exception $e) {
  $this-&amp;gt;connection-&amp;gt;rollbackSavepoint();
  throw $e;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;if ($this-&amp;gt;connection-&amp;gt;inTransaction()) {
  $savepoint = $this-&amp;gt;connection-&amp;gt;startTransaction('mimic_implicit_commit');
}
try {
  $result = do_something();
  if (isset($savepoint)) {
    $savepoint-&amp;gt;commitOrRelease();
  }
}
catch (\Exception $e) {
  if (isset($savepoint)) {
    $savepoint-&amp;gt;rollback();
  }
  throw $e;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 9, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 11, 2026 (2 days and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;andypost (Skilld)&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3406985: Convert all transactions in core to use explicit ::commitOrRelease()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3406985.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0ee1f6ee0120</guid>
      <pubDate>Sat, 11 Apr 2026 21:29:54 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/8d78571bbef839c9344ac04aea0ea428d43dc823"&gt;8d78571&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3406985"&gt;#3406985&lt;/a&gt; · 8 contributors · 63 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 21&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;All database transactions across Drupal core now use explicit &lt;code&gt;$transaction-&amp;gt;commitOrRelease()&lt;/code&gt; instead of relying on implicit commit when the &lt;code&gt;Transaction&lt;/code&gt; object goes out of scope or is unset. This is an internal change with no direct user-facing effect, but it lays the groundwork for deprecating the implicit commit-on-destruct behavior and resolving a class of failures caused by unpredictable object destruction order (particularly with xdebug 3.3+ in develop mode).&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Drupal's database layer wraps operations in &lt;code&gt;Transaction&lt;/code&gt; objects. Historically, these transactions were committed when the object was destroyed (going out of scope or via &lt;code&gt;unset($transaction)&lt;/code&gt;). This &amp;quot;autocommit on destruct&amp;quot; pattern is fragile because PHP does not guarantee destruction order during shutdown, and xdebug 3.3+ in develop mode changes destruction order enough to cause failures. The parent issue &lt;a href="https://www.drupal.org/node/3398767"&gt;#3398767&lt;/a&gt; introduced &lt;code&gt;Transaction::commitOrRelease()&lt;/code&gt; as an explicit alternative; this issue converts every transaction site in core to use it.&lt;/p&gt;
&lt;p&gt;The commit touches 15 production files across the config, database, entity, menu, routing, locale, sqlite, and workspaces subsystems. The changes follow a few patterns:&lt;/p&gt;
&lt;p&gt;Where core previously used &lt;code&gt;unset($transaction)&lt;/code&gt; as an intentional commit trigger (in &lt;code&gt;ExportStorageManager&lt;/code&gt;, &lt;code&gt;ImportStorageTransformer&lt;/code&gt;, &lt;code&gt;PoDatabaseWriter&lt;/code&gt;, and &lt;code&gt;WorkspaceTracker::moveTrackedEntities&lt;/code&gt;), those calls are replaced with &lt;code&gt;$transaction-&amp;gt;commitOrRelease()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In try/catch blocks where transactions were committed implicitly by falling through (e.g. &lt;code&gt;SqlContentEntityStorage::save()&lt;/code&gt;, &lt;code&gt;SqlContentEntityStorage::delete()&lt;/code&gt;, &lt;code&gt;SqlContentEntityStorage::restore()&lt;/code&gt;, &lt;code&gt;MenuTreeStorage::doSave()&lt;/code&gt;, &lt;code&gt;MatcherDumper::dump()&lt;/code&gt;, &lt;code&gt;Insert::execute()&lt;/code&gt;, &lt;code&gt;WorkspaceMerger::merge()&lt;/code&gt;, &lt;code&gt;WorkspacePublisher::publish()&lt;/code&gt;), an explicit &lt;code&gt;$transaction-&amp;gt;commitOrRelease()&lt;/code&gt; is added before the end of the try block.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;SqlContentEntityStorage&lt;/code&gt;, the catch blocks now wrap &lt;code&gt;$transaction-&amp;gt;rollBack()&lt;/code&gt; calls in an additional try/catch for &lt;code&gt;TransactionOutOfOrderException&lt;/code&gt;, because after an explicit commit the transaction may already be resolved when the catch block runs. Similarly, &lt;code&gt;MenuRouterRebuildSubscriber::menuLinksRebuild()&lt;/code&gt; adds a &lt;code&gt;$this-&amp;gt;connection-&amp;gt;inTransaction()&lt;/code&gt; guard before attempting rollback.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;TransactionManagerBase::unpile()&lt;/code&gt;, an early return is added when &lt;code&gt;getConnectionTransactionState()&lt;/code&gt; returns &lt;code&gt;ClientConnectionTransactionState::Committed&lt;/code&gt;. This makes &lt;code&gt;commitOrRelease()&lt;/code&gt; a safe no-op when called on a transaction that was already committed (e.g. by a prior DDL statement on a non-transactional-DDL database), instead of throwing &lt;code&gt;TransactionOutOfOrderException&lt;/code&gt;. A minor fix in &lt;code&gt;TransactionManagerBase::rollback()&lt;/code&gt; also changes a direct property assignment to use the &lt;code&gt;setConnectionTransactionState()&lt;/code&gt; setter.&lt;/p&gt;
&lt;p&gt;Test updates in &lt;code&gt;TransactionTest&lt;/code&gt; and &lt;code&gt;DeleteTruncateTest&lt;/code&gt; convert implicit commit patterns to use &lt;code&gt;commitOrRelease()&lt;/code&gt;, and relax expectations that previously required &lt;code&gt;TransactionOutOfOrderException&lt;/code&gt; in scenarios where the transaction was already committed. Deprecation of the implicit commit behavior is deferred to &lt;a href="https://www.drupal.org/node/3584238"&gt;#3584238&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: December 8, 2023&lt;/li&gt;
&lt;li&gt;Committed: April 11, 2026 (2 years and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mondrake&lt;/li&gt;
&lt;li&gt;mradcliffe (Kosada)&lt;/li&gt;
&lt;li&gt;ghost of drupal past&lt;/li&gt;
&lt;li&gt;godotislate&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;mondrake worked on this issue for over two years, maintaining the MR through repeated rebases. After the full scope (conversions plus deprecations) stalled in review, mondrake opened a second MR with reduced scope covering only the conversion of core transactions to explicit commit, deferring the deprecation work to &lt;a href="https://www.drupal.org/node/3584238"&gt;#3584238&lt;/a&gt;. This unblocked progress. ghost of drupal past raised a question about whether the historical decision in &lt;a href="https://www.drupal.org/node/301049"&gt;#301049&lt;/a&gt; to prohibit explicit commits had been considered. amateescu investigated and confirmed that the prohibition in &lt;a href="https://www.drupal.org/node/301049"&gt;#301049&lt;/a&gt; was a safeguard against bypassing the nesting layer via raw &lt;code&gt;PDO::commit()&lt;/code&gt;, not a design objection to explicit commits through the managed API, and committed the change.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3548957: Deprecate ToStringTrait</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3548957.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0f106a768fc5</guid>
      <category>Module maintainers</category>
      <pubDate>Fri, 10 Apr 2026 20:06:48 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/671cd636121616a0ce5232ceddc68e25ccb11263"&gt;671cd63&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3548957"&gt;#3548957&lt;/a&gt; · 5 contributors · 25 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ToStringTrait&lt;/code&gt; is deprecated. The trait existed to work around a PHP bug (fixed in PHP 7.4) that prevented &lt;code&gt;__toString()&lt;/code&gt; from throwing exceptions. Since Drupal requires PHP 8.2+, the workaround is unnecessary. The two classes that used the trait, &lt;code&gt;DateTimePlus&lt;/code&gt; and &lt;code&gt;TranslatableMarkup&lt;/code&gt;, now implement &lt;code&gt;__toString()&lt;/code&gt; directly. This is an internal change with no visible effect on site behavior.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; &lt;code&gt;DateTimePlus&lt;/code&gt; and &lt;code&gt;TranslatableMarkup&lt;/code&gt; no longer use &lt;code&gt;ToStringTrait&lt;/code&gt;; exceptions in &lt;code&gt;__toString()&lt;/code&gt; now propagate instead of being converted to &lt;code&gt;trigger_error()&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; &lt;code&gt;ToStringTrait&lt;/code&gt; deprecated in 11.4.0, removed in 13.0.0; implement &lt;code&gt;__toString()&lt;/code&gt; directly instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ToStringTrait&lt;/code&gt; provided a &lt;code&gt;__toString()&lt;/code&gt; that caught exceptions and converted them into &lt;code&gt;trigger_error()&lt;/code&gt; calls, because older PHP versions made it fatal for &lt;code&gt;__toString()&lt;/code&gt; to throw. The trait also exposed a &lt;code&gt;_die()&lt;/code&gt; method used only to make tests possible. Since PHP 7.4 lifted that restriction, the entire mechanism is dead code.&lt;/p&gt;
&lt;p&gt;The trait had exactly two users: &lt;code&gt;DateTimePlus&lt;/code&gt; and &lt;code&gt;TranslatableMarkup&lt;/code&gt;. Both now have their own &lt;code&gt;__toString()&lt;/code&gt; that calls &lt;code&gt;(string) $this-&amp;gt;render()&lt;/code&gt; without exception handling. &lt;code&gt;DateTimePlus&lt;/code&gt; explicitly implements &lt;code&gt;\Stringable&lt;/code&gt;. &lt;code&gt;TranslatableMarkup&lt;/code&gt; already satisfies &lt;code&gt;\Stringable&lt;/code&gt; through its parent &lt;code&gt;FormattableMarkup&lt;/code&gt;, which implements &lt;code&gt;MarkupInterface&lt;/code&gt; (extends &lt;code&gt;\Stringable&lt;/code&gt;). Its new &lt;code&gt;__toString()&lt;/code&gt; overrides the parent's version to call &lt;code&gt;render()&lt;/code&gt; instead of &lt;code&gt;placeholderFormat()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Drupal error handler in &lt;code&gt;errors.inc&lt;/code&gt; had a special case (&lt;code&gt;$to_string&lt;/code&gt;) that treated &lt;code&gt;E_USER_ERROR&lt;/code&gt; from &lt;code&gt;__toString()&lt;/code&gt; as fatal. This was removed because the trait no longer emits those errors.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ToStringTrait&lt;/code&gt; file is kept but triggers &lt;code&gt;E_USER_DEPRECATED&lt;/code&gt; at file load time, scheduled for removal in drupal:13.0.0. The PHPStan baseline entries for &lt;code&gt;_die()&lt;/code&gt; on both classes were removed. &lt;code&gt;TranslatableMarkupTest&lt;/code&gt; was simplified: instead of a custom error handler verifying that exceptions become &lt;code&gt;E_USER_WARNING&lt;/code&gt;, it uses &lt;code&gt;expectException()&lt;/code&gt; to confirm that exceptions now propagate normally.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Replace &lt;code&gt;use ToStringTrait;&lt;/code&gt; with a direct &lt;code&gt;__toString()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Before
use Drupal\Component\Utility\ToStringTrait;

class MyClass {
  use ToStringTrait;

  public function render() {
    return 'output';
  }
}

// After
class MyClass implements \Stringable {
  public function render() {
    return 'output';
  }

  public function __toString(): string {
    return (string) $this-&amp;gt;render();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: September 26, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 10, 2026 (6 months and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;andypost&lt;/li&gt;
&lt;li&gt;chi&lt;/li&gt;
&lt;li&gt;dcam&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3301682: Define bundle classes via attributes</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3301682.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0edaf648f380</guid>
      <category>Module maintainers</category>
      <pubDate>Fri, 10 Apr 2026 12:19:41 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; main&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/ff835241c95ec7d815b882e8c3087d79b0415ecb"&gt;ff83524&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3301682"&gt;#3301682&lt;/a&gt; · 14 contributors · 77 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 33&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Bundle classes can now be registered by placing a &lt;code&gt;#[Bundle]&lt;/code&gt; attribute on a class in a module's &lt;code&gt;Entity/&lt;/code&gt; namespace, instead of implementing &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt;. This removes boilerplate when a project has many bundle classes spread across many modules. The attribute accepts &lt;code&gt;entityType&lt;/code&gt;, &lt;code&gt;bundle&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, and &lt;code&gt;translatable&lt;/code&gt; parameters. The &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt; approach still works and can override attribute-provided values.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;\Drupal\Core\Entity\Attribute\Bundle&lt;/code&gt; attribute for declaring bundle classes without &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Declaring a bundle class previously required implementing &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt; and setting the class for each bundle. With many bundle classes across many modules, this meant many hook implementations and &lt;code&gt;use&lt;/code&gt; statements in &lt;code&gt;.module&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;\Drupal\Core\Entity\Attribute\Bundle&lt;/code&gt; attribute extends &lt;code&gt;ContentEntityType&lt;/code&gt;, which itself extends &lt;code&gt;EntityType&lt;/code&gt;. Because &lt;code&gt;EntityTypeManager&lt;/code&gt; already discovers classes with &lt;code&gt;EntityType&lt;/code&gt; attributes under each module's &lt;code&gt;Entity/&lt;/code&gt; subdirectory (recursively), bundle classes are picked up by the existing &lt;code&gt;AttributeDiscoveryWithAnnotations&lt;/code&gt; without introducing a new discovery mechanism. The issue explored three other approaches (a plugin manager, a compiler pass, and a standalone discovery class in &lt;code&gt;EntityTypeManager::findDefinitions()&lt;/code&gt;) before settling on this derivative-based design proposed by ghost of drupal past.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Bundle&lt;/code&gt; constructor formats its ID as &lt;code&gt;{entity_type_id}:{bundle}&lt;/code&gt; using the plugin derivative separator. In &lt;code&gt;EntityTypeManager::findDefinitions()&lt;/code&gt;, definitions whose IDs contain &lt;code&gt;:&lt;/code&gt; are filtered out of the normal entity type definitions. Their bundle class, label, and translatable info is collected and attached to the parent entity type definition via the &lt;code&gt;entity_type_bundle_info&lt;/code&gt; property. &lt;code&gt;EntityTypeBundleInfo::getAllBundleInfo()&lt;/code&gt; then reads that property and merges it into bundle info. The processing order is: &lt;code&gt;hook_entity_bundle_info&lt;/code&gt; runs first, then attribute bundle info is applied, then &lt;code&gt;hook_entity_bundle_info_alter&lt;/code&gt; runs. Optional attribute properties (&lt;code&gt;label&lt;/code&gt;, &lt;code&gt;translatable&lt;/code&gt;) that are &lt;code&gt;NULL&lt;/code&gt; do not override values already set by hooks.&lt;/p&gt;
&lt;p&gt;For entity types that define a &lt;code&gt;bundle_entity_type&lt;/code&gt; (e.g. nodes use &lt;code&gt;node_type&lt;/code&gt;), the attribute cannot create new bundles. If the bundle entity does not exist, the attribute info is silently skipped (per berdir's suggestion in #65-#69). This prevents invalid states where bundles exist without their expected config entities.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EntityType::__construct()&lt;/code&gt; was updated to handle derivative IDs: if the ID contains &lt;code&gt;:&lt;/code&gt;, the entity type part and the bundle part are validated separately against &lt;code&gt;ID_MAX_LENGTH&lt;/code&gt; and &lt;code&gt;BUNDLE_MAX_LENGTH&lt;/code&gt; (both 32 characters). &lt;code&gt;EntityTypeManager::getDefinition()&lt;/code&gt; throws a &lt;code&gt;\LogicException&lt;/code&gt; if called with a derivative ID, preventing bundle definitions from being accessed as entity types.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;To use the new attribute, place &lt;code&gt;#[Bundle]&lt;/code&gt; on a class extending the entity type's base class in your module's &lt;code&gt;Entity/&lt;/code&gt; namespace:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;use Drupal\Core\Entity\Attribute\Bundle;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\node\Entity\Node;

#[Bundle(
  entityType: 'node',
  bundle: 'article',
  label: new TranslatableMarkup('Article'),
)]
class Article extends Node {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The corresponding &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt; implementation can then be removed. The &lt;code&gt;hook_entity_bundle_info_alter()&lt;/code&gt; still works and takes precedence over attributes, so both approaches can coexist.&lt;/p&gt;
&lt;p&gt;Projects migrating from the contrib &lt;a href="https://www.drupal.org/project/bca"&gt;Bundle Class Attribute&lt;/a&gt; module need to update the attribute namespace and rename the &lt;code&gt;entityType&lt;/code&gt; parameter from &lt;code&gt;entityType&lt;/code&gt; to &lt;code&gt;entity_type_id&lt;/code&gt; if the contrib module used that name (or verify the parameter name matches).&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: August 4, 2022&lt;/li&gt;
&lt;li&gt;Committed: April 10, 2026 (3 years and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;dpi (PreviousNext)&lt;/li&gt;
&lt;li&gt;mstrelan (PreviousNext)&lt;/li&gt;
&lt;li&gt;avpaderno&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;godotislate&lt;/li&gt;
&lt;li&gt;ghost of drupal past&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;li&gt;acbramley (PreviousNext)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;mstrelan opened the issue and created the first three merge requests exploring different discovery approaches (plugin manager, compiler pass, standalone discovery). godotislate then proposed a fourth approach in MR 14763, and ghost of drupal past proposed the derivative-based approach in MR 14764, which godotislate implemented. berdir identified the problem of attributes creating bundles for entity types with &lt;code&gt;bundle_entity_type&lt;/code&gt; (like nodes), leading to the safeguard that silently skips nonexistent bundle entities.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3226806: Move filter implementations from filter.module to plugin classes</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3226806.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0ed7270f9907</guid>
      <category>Module maintainers</category>
      <pubDate>Fri, 10 Apr 2026 10:34:44 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/ec8c0566862b535991b6d037a862cc37f271e13e"&gt;ec8c056&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3226806"&gt;#3226806&lt;/a&gt; · 6 contributors · 52 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 9&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Nine procedural functions in &lt;code&gt;filter.module&lt;/code&gt; are deprecated and their logic moved into the corresponding filter plugin classes. This is part of the broader effort to eliminate core &lt;code&gt;.module&lt;/code&gt; files (&lt;a href="https://www.drupal.org/node/3566536"&gt;#3566536&lt;/a&gt;). The deprecated functions still work during the deprecation period by delegating to the plugin manager. No direct replacement is provided for calling the functions; callers should use the filter plugin manager instead.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; &lt;code&gt;FilterUrl&lt;/code&gt; and &lt;code&gt;FilterHtmlImageSecure&lt;/code&gt; now implement &lt;code&gt;ContainerFactoryPluginInterface&lt;/code&gt; with new constructor parameters (BC layer falls back to service container with deprecation notice until 12.0.0)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecation:&lt;/strong&gt; Nine procedural functions in &lt;code&gt;filter.module&lt;/code&gt; (&lt;code&gt;_filter_url&lt;/code&gt;, &lt;code&gt;_filter_url_parse_full_links&lt;/code&gt;, &lt;code&gt;_filter_url_parse_email_links&lt;/code&gt;, &lt;code&gt;_filter_url_parse_partial_links&lt;/code&gt;, &lt;code&gt;_filter_url_escape_comments&lt;/code&gt;, &lt;code&gt;_filter_url_trim&lt;/code&gt;, &lt;code&gt;_filter_autop&lt;/code&gt;, &lt;code&gt;_filter_html_escape&lt;/code&gt;, &lt;code&gt;_filter_html_image_secure_process&lt;/code&gt;) deprecated in 11.4.0 for removal in 13.0.0; logic moved to &lt;code&gt;FilterUrl&lt;/code&gt;, &lt;code&gt;FilterAutoP&lt;/code&gt;, &lt;code&gt;FilterHtmlEscape&lt;/code&gt;, and &lt;code&gt;FilterHtmlImageSecure&lt;/code&gt; plugin classes with no direct replacement&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;When filters were converted to plugins in &lt;a href="https://www.drupal.org/node/1868772"&gt;#1868772&lt;/a&gt;, the plugin &lt;code&gt;process()&lt;/code&gt; methods were thin wrappers that called back into procedural functions in &lt;code&gt;filter.module&lt;/code&gt;. For example, &lt;code&gt;FilterAutoP::process()&lt;/code&gt; just called &lt;code&gt;_filter_autop()&lt;/code&gt;. This change moves the actual logic into the plugin classes and deprecates the procedural functions.&lt;/p&gt;
&lt;p&gt;Four plugin classes are affected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FilterAutoP&lt;/code&gt;: The line-break-to-paragraph logic from &lt;code&gt;_filter_autop()&lt;/code&gt; is inlined directly into &lt;code&gt;process()&lt;/code&gt;. No dependency injection needed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FilterHtmlEscape&lt;/code&gt;: The one-liner &lt;code&gt;_filter_html_escape()&lt;/code&gt; body (&lt;code&gt;trim(Html::escape($text))&lt;/code&gt;) is inlined into &lt;code&gt;process()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FilterUrl&lt;/code&gt;: The most complex change. The class now implements &lt;code&gt;ContainerFactoryPluginInterface&lt;/code&gt; and receives the &lt;code&gt;filter_protocols&lt;/code&gt; container parameter via &lt;code&gt;#[Autowire(param: 'filter_protocols')]&lt;/code&gt;. The main URL-matching logic from &lt;code&gt;_filter_url()&lt;/code&gt; moves into &lt;code&gt;process()&lt;/code&gt;. Six helper functions become protected methods: &lt;code&gt;parseFullLinks()&lt;/code&gt;, &lt;code&gt;parseEmailLinks()&lt;/code&gt;, &lt;code&gt;parsePartialLinks()&lt;/code&gt;, &lt;code&gt;escapeComments()&lt;/code&gt;, &lt;code&gt;unescapeComments()&lt;/code&gt;, and &lt;code&gt;trimUrl()&lt;/code&gt;. The old &lt;code&gt;_filter_url_escape_comments()&lt;/code&gt; used a static variable to toggle between escape and unescape modes; this was split into two separate methods using an &lt;code&gt;$htmlComments&lt;/code&gt; instance property for temporary storage. The old &lt;code&gt;_filter_url_trim()&lt;/code&gt; used a static variable to store the URL length; &lt;code&gt;trimUrl()&lt;/code&gt; reads directly from &lt;code&gt;$this-&amp;gt;settings['filter_url_length']&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FilterHtmlImageSecure&lt;/code&gt;: Now implements &lt;code&gt;ContainerFactoryPluginInterface&lt;/code&gt; and injects &lt;code&gt;FileUrlGeneratorInterface&lt;/code&gt;, &lt;code&gt;ModuleHandlerInterface&lt;/code&gt;, and the &lt;code&gt;app.root&lt;/code&gt; parameter, replacing the &lt;code&gt;\Drupal::service()&lt;/code&gt; and &lt;code&gt;\Drupal::root()&lt;/code&gt; calls that were in &lt;code&gt;_filter_html_image_secure_process()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The deprecated functions (&lt;code&gt;_filter_url&lt;/code&gt;, &lt;code&gt;_filter_autop&lt;/code&gt;, &lt;code&gt;_filter_html_escape&lt;/code&gt;, &lt;code&gt;_filter_html_image_secure_process&lt;/code&gt;) delegate to the appropriate plugin via the plugin manager so the code exists in only one place, per joachim's suggestion. The smaller callback functions (&lt;code&gt;_filter_url_parse_full_links&lt;/code&gt;, &lt;code&gt;_filter_url_parse_email_links&lt;/code&gt;, &lt;code&gt;_filter_url_parse_partial_links&lt;/code&gt;, &lt;code&gt;_filter_url_escape_comments&lt;/code&gt;, &lt;code&gt;_filter_url_trim&lt;/code&gt;) retain their original logic inline since they cannot easily delegate to the plugin instance.&lt;/p&gt;
&lt;p&gt;Tests in &lt;code&gt;FilterKernelTest&lt;/code&gt; are updated to instantiate plugins via the plugin manager instead of calling the deprecated procedural functions.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Callers of &lt;code&gt;_filter_autop()&lt;/code&gt;, &lt;code&gt;_filter_html_escape()&lt;/code&gt;, or &lt;code&gt;_filter_html_image_secure_process()&lt;/code&gt; should use the plugin manager:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;$result = \Drupal::service('plugin.manager.filter')
  -&amp;gt;createInstance('filter_autop')
  -&amp;gt;process($text, $langcode)
  -&amp;gt;getProcessedText();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Callers of &lt;code&gt;_filter_url()&lt;/code&gt; should pass settings when creating the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;$result = \Drupal::service('plugin.manager.filter')
  -&amp;gt;createInstance('filter_url', ['settings' =&amp;gt; ['filter_url_length' =&amp;gt; 72]])
  -&amp;gt;process($text, $langcode)
  -&amp;gt;getProcessedText();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No replacement is provided for &lt;code&gt;_filter_url_parse_full_links()&lt;/code&gt;, &lt;code&gt;_filter_url_parse_email_links()&lt;/code&gt;, &lt;code&gt;_filter_url_parse_partial_links()&lt;/code&gt;, &lt;code&gt;_filter_url_escape_comments()&lt;/code&gt;, or &lt;code&gt;_filter_url_trim()&lt;/code&gt;. These were internal callbacks.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: August 4, 2021&lt;/li&gt;
&lt;li&gt;Committed: April 10, 2026 (4 years and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;claudiu.cristea (Webikon)&lt;/li&gt;
&lt;li&gt;nicxvan (nLightened Development LLC)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;joachim (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;li&gt;larowlan (PreviousNext)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;This issue was opened by longwave in 2021 as a simple cleanup but sat idle for years. joachim initially argued underscore-prefixed functions could be removed without deprecation since they were always considered private, but claudiu.cristea pushed back noting that third-party code uses them (especially &lt;code&gt;_filter_autop&lt;/code&gt;), establishing the deprecation approach. joachim later suggested having the deprecated functions delegate to the plugin so the logic only exists in one place, which shaped the final design. claudiu.cristea drove the implementation, creating a fresh MR after the original could not be rebased due to conflicts in deleted code.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3581817: Move uses of Shortcut from Jsonapi to Shortcut</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3581817.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-95123f53440d</guid>
      <pubDate>Thu, 09 Apr 2026 21:44:59 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/b4c833bf0fdd82073c05bdc24128ab572cacf987"&gt;b4c833b&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3581817"&gt;#3581817&lt;/a&gt; · 5 contributors · 26 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is an internal code reorganization with no user-facing effect. Shortcut-specific filter access logic that lived in the JSON:API module was moved to the Shortcut module, so the Shortcut module now owns its own access restrictions for JSON:API queries.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;TemporaryQueryGuard&lt;/code&gt; in JSON:API has a switch statement with hard-coded access conditions for several core entity types. Its own documentation notes that modules should implement &lt;code&gt;hook_query_ENTITY_TYPE_access_alter()&lt;/code&gt; instead, and that the hard-coded cases exist only because some core entity types have not done so yet. The shortcut case restricted non-admin users to only query shortcuts from their currently displayed shortcut set.&lt;/p&gt;
&lt;p&gt;This change removes the &lt;code&gt;case 'shortcut'&lt;/code&gt; from &lt;code&gt;TemporaryQueryGuard::getAccessCondition()&lt;/code&gt; and the &lt;code&gt;hook_jsonapi_shortcut_filter_access&lt;/code&gt; implementation from &lt;code&gt;JsonapiHooks&lt;/code&gt;. Both are moved to &lt;code&gt;ShortcutHooks&lt;/code&gt; in the shortcut module.&lt;/p&gt;
&lt;p&gt;The filter access hook (&lt;code&gt;jsonapiShortcutFilterAccess&lt;/code&gt;) moves with a small difference: the new version adds &lt;code&gt;-&amp;gt;addCacheContexts(['user'])&lt;/code&gt; to the access result, which was previously handled by the &lt;code&gt;TemporaryQueryGuard&lt;/code&gt; code path that set cacheability on the condition side.&lt;/p&gt;
&lt;p&gt;The query restriction (limiting results to the user's displayed shortcut set) is now implemented as &lt;code&gt;hook_query_shortcut_access_alter&lt;/code&gt; via the new &lt;code&gt;queryShortcutAccessAlter()&lt;/code&gt; method in &lt;code&gt;ShortcutHooks&lt;/code&gt;. This follows the &lt;code&gt;hook_query_TAG_alter&lt;/code&gt; pattern documented in &lt;code&gt;TemporaryQueryGuard&lt;/code&gt;. The new implementation finds the &lt;code&gt;shortcut_field_data&lt;/code&gt; base table and adds a condition on &lt;code&gt;shortcut_set&lt;/code&gt; to match the user's displayed set.&lt;/p&gt;
&lt;p&gt;A stale &lt;code&gt;@var&lt;/code&gt; annotation in &lt;code&gt;WorkflowTest&lt;/code&gt; that incorrectly referenced &lt;code&gt;\Drupal\shortcut\ShortcutSetInterface&lt;/code&gt; was also fixed to &lt;code&gt;\Drupal\workflows\WorkflowInterface&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 27, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (13 days and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;sourav_paul (Innoraft)&lt;/li&gt;
&lt;li&gt;smustgrave (Mobomo)&lt;/li&gt;
&lt;li&gt;quietone (PreviousNext)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;amateescu&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;An initial approach by sourav_paul introduced a new JSON:API API surface (static helper and new hook) to let shortcut own the logic. smustgrave and catch pushed an alternative that avoided adding new API. longwave then discussed the approach with smustgrave on Slack and produced the final merge request, which uses the existing &lt;code&gt;hook_query_TAG_alter&lt;/code&gt; pattern already documented in &lt;code&gt;TemporaryQueryGuard&lt;/code&gt;. The participants agreed this narrower approach was better than introducing new JSON:API API just to move one module's logic.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3457818: Optimize CacheContextsManager:::convertTokensToKeys()/optimizeTokens()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3457818.md</link>
      <guid isPermaLink="false">019d8b26-30ef-711b-842d-aa65ec70c9c2</guid>
      <pubDate>Thu, 09 Apr 2026 13:20:56 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/160ab21c68d53d6548c985a06d573e2cf72dd638"&gt;160ab21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3457818"&gt;#3457818&lt;/a&gt; · 9 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 15&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Sites with many cache context lookups per request, such as those using the Group module or the Access Policy API, were spending measurable CPU time inside &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt;. This change makes that method significantly faster, reducing time spent there by roughly half in real-world profiling, with no change to behavior or public APIs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The bottleneck was in &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt; in &lt;code&gt;core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php&lt;/code&gt;. Three algorithmic improvements were made:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early return for single-token arrays.&lt;/strong&gt; When &lt;code&gt;count($context_tokens) &amp;lt;= 1&lt;/code&gt;, no ancestor can exist and no optimization is possible, so the method returns immediately instead of entering the loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(1) ancestor lookup via hash table.&lt;/strong&gt; The old code used &lt;code&gt;in_array($ancestor, $context_tokens)&lt;/code&gt; (linear scan) to check whether an ancestor token was present. The new code builds &lt;code&gt;$context_tokens_lookup = array_flip($context_tokens)&lt;/code&gt; once before the loop and uses &lt;code&gt;isset($context_tokens_lookup[$ancestor])&lt;/code&gt;, reducing each ancestor check from O(n) to O(1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;getService()&lt;/code&gt; call.&lt;/strong&gt; Previously, every token that contained a period or colon triggered a call to &lt;code&gt;$this-&amp;gt;getService($context_id)-&amp;gt;getCacheableMetadata($parameter)-&amp;gt;getCacheMaxAge()&lt;/code&gt; before checking for ancestors. Now the cheap string-based ancestor search runs first, and &lt;code&gt;getService()&lt;/code&gt; is called only when an ancestor is actually confirmed. In one profiling session this dropped &lt;code&gt;getService&lt;/code&gt; calls from 3,775 to only those cases where an ancestor exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The change deliberately avoids caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results, because cache context values can change during a request. That optimization (safe at minimum for anonymous users) is tracked in the companion issue &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The unit test &lt;code&gt;CacheContextsManagerTest::testOptimizeTokens()&lt;/code&gt; was updated to assert exact container call counts using &lt;code&gt;$this-&amp;gt;exactly($expected_container_calls)&lt;/code&gt;, replacing the old &lt;code&gt;$this-&amp;gt;any()&lt;/code&gt; stub. The &lt;code&gt;StandardPerformanceTest&lt;/code&gt; was updated to reflect three fewer cache gets per measured scenario. The &lt;code&gt;getMockContainer()&lt;/code&gt; helper was also converted from a mock to a stub since it no longer needs call-count expectations.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: June 28, 2024&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 year and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kristiaanvandeneynde (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;tibezh&lt;/li&gt;
&lt;li&gt;klausi (jobiqo - job board technology)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The design went through one substantive shift. tibezh's initial patch included caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results in a class property, arguing the metadata is static. kristiaanvandeneynde blocked that specific change, pointing out that cache context values can legitimately change during a request and that caching them could produce incorrect optimization decisions. The final patch dropped the metadata cache entirely. catch later argued the concern may not apply to anonymous users and that a static cache is probably safe in that scope, but the two contributors agreed to pursue that improvement separately in &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt; rather than complicate this fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3457818: Optimize CacheContextsManager:::convertTokensToKeys()/optimizeTokens()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3457818.md</link>
      <guid isPermaLink="false">019d8b26-30ef-711b-842d-aa65ec70c9c2</guid>
      <pubDate>Thu, 09 Apr 2026 13:20:56 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/160ab21c68d53d6548c985a06d573e2cf72dd638"&gt;160ab21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3457818"&gt;#3457818&lt;/a&gt; · 9 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 15&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Sites with many cache context lookups per request, such as those using the Group module or the Access Policy API, were spending measurable CPU time inside &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt;. This change makes that method significantly faster, reducing time spent there by roughly half in real-world profiling, with no change to behavior or public APIs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The bottleneck was in &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt; in &lt;code&gt;core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php&lt;/code&gt;. Three algorithmic improvements were made:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early return for single-token arrays.&lt;/strong&gt; When &lt;code&gt;count($context_tokens) &amp;lt;= 1&lt;/code&gt;, no ancestor can exist and no optimization is possible, so the method returns immediately instead of entering the loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(1) ancestor lookup via hash table.&lt;/strong&gt; The old code used &lt;code&gt;in_array($ancestor, $context_tokens)&lt;/code&gt; (linear scan) to check whether an ancestor token was present. The new code builds &lt;code&gt;$context_tokens_lookup = array_flip($context_tokens)&lt;/code&gt; once before the loop and uses &lt;code&gt;isset($context_tokens_lookup[$ancestor])&lt;/code&gt;, reducing each ancestor check from O(n) to O(1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;getService()&lt;/code&gt; call.&lt;/strong&gt; Previously, every token that contained a period or colon triggered a call to &lt;code&gt;$this-&amp;gt;getService($context_id)-&amp;gt;getCacheableMetadata($parameter)-&amp;gt;getCacheMaxAge()&lt;/code&gt; before checking for ancestors. Now the cheap string-based ancestor search runs first, and &lt;code&gt;getService()&lt;/code&gt; is called only when an ancestor is actually confirmed. In one profiling session this dropped &lt;code&gt;getService&lt;/code&gt; calls from 3,775 to only those cases where an ancestor exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The change deliberately avoids caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results, because cache context values can change during a request. That optimization (safe at minimum for anonymous users) is tracked in the companion issue &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The unit test &lt;code&gt;CacheContextsManagerTest::testOptimizeTokens()&lt;/code&gt; was updated to assert exact container call counts using &lt;code&gt;$this-&amp;gt;exactly($expected_container_calls)&lt;/code&gt;, replacing the old &lt;code&gt;$this-&amp;gt;any()&lt;/code&gt; stub. The &lt;code&gt;StandardPerformanceTest&lt;/code&gt; was updated to reflect three fewer cache gets per measured scenario. The &lt;code&gt;getMockContainer()&lt;/code&gt; helper was also converted from a mock to a stub since it no longer needs call-count expectations.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: June 28, 2024&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 year and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kristiaanvandeneynde (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;tibezh&lt;/li&gt;
&lt;li&gt;klausi (jobiqo - job board technology)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The design went through one substantive shift. tibezh's initial patch included caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results in a class property, arguing the metadata is static. kristiaanvandeneynde blocked that specific change, pointing out that cache context values can legitimately change during a request and that caching them could produce incorrect optimization decisions. The final patch dropped the metadata cache entirely. catch later argued the concern may not apply to anonymous users and that a static cache is probably safe in that scope, but the two contributors agreed to pursue that improvement separately in &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt; rather than complicate this fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3457818: Optimize CacheContextsManager:::convertTokensToKeys()/optimizeTokens()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3457818.md</link>
      <guid isPermaLink="false">019d8b26-30ef-711b-842d-aa65ec70c9c2</guid>
      <pubDate>Thu, 09 Apr 2026 13:20:56 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/160ab21c68d53d6548c985a06d573e2cf72dd638"&gt;160ab21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3457818"&gt;#3457818&lt;/a&gt; · 9 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 15&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Sites with many cache context lookups per request, such as those using the Group module or the Access Policy API, were spending measurable CPU time inside &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt;. This change makes that method significantly faster, reducing time spent there by roughly half in real-world profiling, with no change to behavior or public APIs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The bottleneck was in &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt; in &lt;code&gt;core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php&lt;/code&gt;. Three algorithmic improvements were made:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early return for single-token arrays.&lt;/strong&gt; When &lt;code&gt;count($context_tokens) &amp;lt;= 1&lt;/code&gt;, no ancestor can exist and no optimization is possible, so the method returns immediately instead of entering the loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(1) ancestor lookup via hash table.&lt;/strong&gt; The old code used &lt;code&gt;in_array($ancestor, $context_tokens)&lt;/code&gt; (linear scan) to check whether an ancestor token was present. The new code builds &lt;code&gt;$context_tokens_lookup = array_flip($context_tokens)&lt;/code&gt; once before the loop and uses &lt;code&gt;isset($context_tokens_lookup[$ancestor])&lt;/code&gt;, reducing each ancestor check from O(n) to O(1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;getService()&lt;/code&gt; call.&lt;/strong&gt; Previously, every token that contained a period or colon triggered a call to &lt;code&gt;$this-&amp;gt;getService($context_id)-&amp;gt;getCacheableMetadata($parameter)-&amp;gt;getCacheMaxAge()&lt;/code&gt; before checking for ancestors. Now the cheap string-based ancestor search runs first, and &lt;code&gt;getService()&lt;/code&gt; is called only when an ancestor is actually confirmed. In one profiling session this dropped &lt;code&gt;getService&lt;/code&gt; calls from 3,775 to only those cases where an ancestor exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The change deliberately avoids caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results, because cache context values can change during a request. That optimization (safe at minimum for anonymous users) is tracked in the companion issue &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The unit test &lt;code&gt;CacheContextsManagerTest::testOptimizeTokens()&lt;/code&gt; was updated to assert exact container call counts using &lt;code&gt;$this-&amp;gt;exactly($expected_container_calls)&lt;/code&gt;, replacing the old &lt;code&gt;$this-&amp;gt;any()&lt;/code&gt; stub. The &lt;code&gt;StandardPerformanceTest&lt;/code&gt; was updated to reflect three fewer cache gets per measured scenario. The &lt;code&gt;getMockContainer()&lt;/code&gt; helper was also converted from a mock to a stub since it no longer needs call-count expectations.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: June 28, 2024&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 year and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kristiaanvandeneynde (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;tibezh&lt;/li&gt;
&lt;li&gt;klausi (jobiqo - job board technology)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The design went through one substantive shift. tibezh's initial patch included caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results in a class property, arguing the metadata is static. kristiaanvandeneynde blocked that specific change, pointing out that cache context values can legitimately change during a request and that caching them could produce incorrect optimization decisions. The final patch dropped the metadata cache entirely. catch later argued the concern may not apply to anonymous users and that a static cache is probably safe in that scope, but the two contributors agreed to pursue that improvement separately in &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt; rather than complicate this fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3457818: Optimize CacheContextsManager:::convertTokensToKeys()/optimizeTokens()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3457818.md</link>
      <guid isPermaLink="false">019d8b26-30ef-711b-842d-aa65ec70c9c2</guid>
      <pubDate>Thu, 09 Apr 2026 13:20:56 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/160ab21c68d53d6548c985a06d573e2cf72dd638"&gt;160ab21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3457818"&gt;#3457818&lt;/a&gt; · 9 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 15&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Sites with many cache context lookups per request, such as those using the Group module or the Access Policy API, were spending measurable CPU time inside &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt;. This change makes that method significantly faster, reducing time spent there by roughly half in real-world profiling, with no change to behavior or public APIs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The bottleneck was in &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt; in &lt;code&gt;core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php&lt;/code&gt;. Three algorithmic improvements were made:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early return for single-token arrays.&lt;/strong&gt; When &lt;code&gt;count($context_tokens) &amp;lt;= 1&lt;/code&gt;, no ancestor can exist and no optimization is possible, so the method returns immediately instead of entering the loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(1) ancestor lookup via hash table.&lt;/strong&gt; The old code used &lt;code&gt;in_array($ancestor, $context_tokens)&lt;/code&gt; (linear scan) to check whether an ancestor token was present. The new code builds &lt;code&gt;$context_tokens_lookup = array_flip($context_tokens)&lt;/code&gt; once before the loop and uses &lt;code&gt;isset($context_tokens_lookup[$ancestor])&lt;/code&gt;, reducing each ancestor check from O(n) to O(1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;getService()&lt;/code&gt; call.&lt;/strong&gt; Previously, every token that contained a period or colon triggered a call to &lt;code&gt;$this-&amp;gt;getService($context_id)-&amp;gt;getCacheableMetadata($parameter)-&amp;gt;getCacheMaxAge()&lt;/code&gt; before checking for ancestors. Now the cheap string-based ancestor search runs first, and &lt;code&gt;getService()&lt;/code&gt; is called only when an ancestor is actually confirmed. In one profiling session this dropped &lt;code&gt;getService&lt;/code&gt; calls from 3,775 to only those cases where an ancestor exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The change deliberately avoids caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results, because cache context values can change during a request. That optimization (safe at minimum for anonymous users) is tracked in the companion issue &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The unit test &lt;code&gt;CacheContextsManagerTest::testOptimizeTokens()&lt;/code&gt; was updated to assert exact container call counts using &lt;code&gt;$this-&amp;gt;exactly($expected_container_calls)&lt;/code&gt;, replacing the old &lt;code&gt;$this-&amp;gt;any()&lt;/code&gt; stub. The &lt;code&gt;StandardPerformanceTest&lt;/code&gt; was updated to reflect three fewer cache gets per measured scenario. The &lt;code&gt;getMockContainer()&lt;/code&gt; helper was also converted from a mock to a stub since it no longer needs call-count expectations.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: June 28, 2024&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 year and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kristiaanvandeneynde (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;tibezh&lt;/li&gt;
&lt;li&gt;klausi (jobiqo - job board technology)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The design went through one substantive shift. tibezh's initial patch included caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results in a class property, arguing the metadata is static. kristiaanvandeneynde blocked that specific change, pointing out that cache context values can legitimately change during a request and that caching them could produce incorrect optimization decisions. The final patch dropped the metadata cache entirely. catch later argued the concern may not apply to anonymous users and that a static cache is probably safe in that scope, but the two contributors agreed to pursue that improvement separately in &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt; rather than complicate this fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3457818: Optimize CacheContextsManager:::convertTokensToKeys()/optimizeTokens()</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-core/3457818.md</link>
      <guid isPermaLink="false">019d8b26-30ef-711b-842d-aa65ec70c9c2</guid>
      <pubDate>Thu, 09 Apr 2026 13:20:56 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Core&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 11.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/drupal/-/commit/160ab21c68d53d6548c985a06d573e2cf72dd638"&gt;160ab21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3457818"&gt;#3457818&lt;/a&gt; · 9 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 15&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Sites with many cache context lookups per request, such as those using the Group module or the Access Policy API, were spending measurable CPU time inside &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt;. This change makes that method significantly faster, reducing time spent there by roughly half in real-world profiling, with no change to behavior or public APIs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The bottleneck was in &lt;code&gt;CacheContextsManager::optimizeTokens()&lt;/code&gt; in &lt;code&gt;core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php&lt;/code&gt;. Three algorithmic improvements were made:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early return for single-token arrays.&lt;/strong&gt; When &lt;code&gt;count($context_tokens) &amp;lt;= 1&lt;/code&gt;, no ancestor can exist and no optimization is possible, so the method returns immediately instead of entering the loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(1) ancestor lookup via hash table.&lt;/strong&gt; The old code used &lt;code&gt;in_array($ancestor, $context_tokens)&lt;/code&gt; (linear scan) to check whether an ancestor token was present. The new code builds &lt;code&gt;$context_tokens_lookup = array_flip($context_tokens)&lt;/code&gt; once before the loop and uses &lt;code&gt;isset($context_tokens_lookup[$ancestor])&lt;/code&gt;, reducing each ancestor check from O(n) to O(1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;getService()&lt;/code&gt; call.&lt;/strong&gt; Previously, every token that contained a period or colon triggered a call to &lt;code&gt;$this-&amp;gt;getService($context_id)-&amp;gt;getCacheableMetadata($parameter)-&amp;gt;getCacheMaxAge()&lt;/code&gt; before checking for ancestors. Now the cheap string-based ancestor search runs first, and &lt;code&gt;getService()&lt;/code&gt; is called only when an ancestor is actually confirmed. In one profiling session this dropped &lt;code&gt;getService&lt;/code&gt; calls from 3,775 to only those cases where an ancestor exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The change deliberately avoids caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results, because cache context values can change during a request. That optimization (safe at minimum for anonymous users) is tracked in the companion issue &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The unit test &lt;code&gt;CacheContextsManagerTest::testOptimizeTokens()&lt;/code&gt; was updated to assert exact container call counts using &lt;code&gt;$this-&amp;gt;exactly($expected_container_calls)&lt;/code&gt;, replacing the old &lt;code&gt;$this-&amp;gt;any()&lt;/code&gt; stub. The &lt;code&gt;StandardPerformanceTest&lt;/code&gt; was updated to reflect three fewer cache gets per measured scenario. The &lt;code&gt;getMockContainer()&lt;/code&gt; helper was also converted from a mock to a stub since it no longer needs call-count expectations.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: June 28, 2024&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 year and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kristiaanvandeneynde (Factorial GmbH)&lt;/li&gt;
&lt;li&gt;berdir (MD Systems GmbH)&lt;/li&gt;
&lt;li&gt;tibezh&lt;/li&gt;
&lt;li&gt;klausi (jobiqo - job board technology)&lt;/li&gt;
&lt;li&gt;longwave (Full Fat Things)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The design went through one substantive shift. tibezh's initial patch included caching &lt;code&gt;getCacheableMetadata()&lt;/code&gt; results in a class property, arguing the metadata is static. kristiaanvandeneynde blocked that specific change, pointing out that cache context values can legitimately change during a request and that caching them could produce incorrect optimization decisions. The final patch dropped the metadata cache entirely. catch later argued the concern may not apply to anonymous users and that a static cache is probably safe in that scope, but the two contributors agreed to pursue that improvement separately in &lt;a href="https://www.drupal.org/node/3582977"&gt;#3582977&lt;/a&gt; rather than complicate this fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
  </channel>
</rss>