<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Drupal Patterns</title>
    <description>Reusable patterns extracted from Drupal issue discussions. Patterns, pitfalls, and solutions for module developers.</description>
    <link>https://github.com/dbuytaert/drupal-digests</link>
    <item>
      <title>Apply all row-reducing conditions to an entity query before calling `-&gt;pager()`</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8b34-6ff3-71a0-bfb4-b3686c8036cc.md</link>
      <guid isPermaLink="false">019d8b34-6ff3-71a0-bfb4-b3686c8036cc</guid>
      <category>reliability</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; Adam Bramley, alexpott, Rafael Payan in &lt;a href="https://www.drupal.org/node/2722307"&gt;#2722307&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; reliability, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pager correctness depends on every condition that narrows the result set being applied inside the query, not in PHP after loading. When code runs &lt;code&gt;-&amp;gt;pager(N)&lt;/code&gt; on an entity query and then filters the loaded objects in a loop (for example, checking &lt;code&gt;isRevisionTranslationAffected()&lt;/code&gt; or &lt;code&gt;hasTranslation()&lt;/code&gt; on each result), the pager counts the raw database rows, not the displayed subset. This produces pages with fewer items than expected and can produce empty pages mid-sequence.&lt;/p&gt;
&lt;p&gt;Move all such conditions into the entity query using &lt;code&gt;-&amp;gt;condition()&lt;/code&gt; before the &lt;code&gt;-&amp;gt;pager()&lt;/code&gt; call. For translatable revision lists, add conditions on the entity type's &lt;code&gt;langcode&lt;/code&gt; key and &lt;code&gt;revision_translation_affected&lt;/code&gt; key. The pager then counts exactly the rows that will be displayed, and pagination behaves correctly across all pages.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Query fetches 50 revisions; PHP then discards non-affecting ones.
$vids = $storage-&amp;gt;getQuery()
  -&amp;gt;allRevisions()
  -&amp;gt;condition('nid', $node-&amp;gt;id())
  -&amp;gt;sort('vid', 'DESC')
  -&amp;gt;pager(50)
  -&amp;gt;execute();

$rows = [];
foreach (array_keys($vids) as $vid) {
  $revision = $storage-&amp;gt;loadRevision($vid);
  if (!$revision-&amp;gt;hasTranslation($langcode)) {
    continue;
  }
  $translation = $revision-&amp;gt;getTranslation($langcode);
  if (!$translation-&amp;gt;isRevisionTranslationAffected()) {
    continue;
  }
  $rows[] = $this-&amp;gt;buildRow($translation);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Filtering happens inside the query so the pager counts only displayable rows.
$entityType = $node-&amp;gt;getEntityType();
$vids = $storage-&amp;gt;getQuery()
  -&amp;gt;allRevisions()
  -&amp;gt;condition($entityType-&amp;gt;getKey('id'), $node-&amp;gt;id())
  -&amp;gt;condition($entityType-&amp;gt;getKey('langcode'), $langcode)
  -&amp;gt;condition($entityType-&amp;gt;getKey('revision_translation_affected'), '1')
  -&amp;gt;sort($entityType-&amp;gt;getKey('revision'), 'DESC')
  -&amp;gt;pager(50)
  -&amp;gt;execute();

$rows = [];
foreach (array_keys($vids) as $vid) {
  $revision = $storage-&amp;gt;loadRevision($vid);
  $rows[] = $this-&amp;gt;buildRow($revision-&amp;gt;getTranslation($langcode));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect PHP files that call &lt;code&gt;isRevisionTranslationAffected()&lt;/code&gt; as a method on a loaded entity object; these are candidates where translation filtering may be happening in PHP instead of inside the query. Cross-check each hit to see whether a &lt;code&gt;pager(&lt;/code&gt; call is present in the same file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rg &amp;quot;isRevisionTranslationAffected\(\)&amp;quot; --type php -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives: method calls inside a &lt;code&gt;foreach&lt;/code&gt; over query results that also use &lt;code&gt;pager()&lt;/code&gt;. False positives: the interface/trait declaration, uses inside storage internals (&lt;code&gt;ContentEntityStorageBase&lt;/code&gt;), and legitimate non-paged filtering. The broader principle (any row-reducing PHP filter applied after a paged query) cannot be caught by a single command; review any loop over paged query results for &lt;code&gt;continue&lt;/code&gt; or &lt;code&gt;if&lt;/code&gt; guards that skip rows.&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>Replace deprecated `uri_callback` on entity types with link templates or a route provider</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8ac3-b383-715f-855f-2eab3b245c79.md</link>
      <guid isPermaLink="false">019d8ac3-b383-715f-855f-2eab3b245c79</guid>
      <category>deprecation</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; eojthebrave, longwave, godotislate in &lt;a href="https://www.drupal.org/node/2667040"&gt;#2667040&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; deprecation, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Entity types that declare &lt;code&gt;uri_callback&lt;/code&gt; in their PHP attribute or annotation are using a mechanism deprecated in Drupal 11.4.0 and scheduled for removal in Drupal 13.0.0. The callback was once the only way to provide a canonical URL for an entity, but the &lt;code&gt;links&lt;/code&gt; array (link templates) and route providers have replaced it for both simple and complex routing needs.&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;uri_callback&lt;/code&gt; with a &lt;code&gt;links&lt;/code&gt; array in the entity type attribute or annotation, declaring named routes such as &lt;code&gt;canonical&lt;/code&gt;, &lt;code&gt;edit-form&lt;/code&gt;, and &lt;code&gt;delete-form&lt;/code&gt;. For entity types with complex or dynamic URL patterns that cannot use static route templates, implement &lt;code&gt;EntityRouteProviderInterface&lt;/code&gt; and register it under &lt;code&gt;handlers&lt;/code&gt; in the entity type definition. Drupal's routing layer resolves the correct URL automatically in both cases. See the change record at https://www.drupal.org/node/3575062 for migration details.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;#[ContentEntityType(
  id: 'article',
  label: new TranslatableMarkup('Article'),
  uri_callback: 'article_uri',
)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;#[ContentEntityType(
  id: 'article',
  label: new TranslatableMarkup('Article'),
  links: [
    'canonical' =&amp;gt; '/article/{article}',
    'edit-form' =&amp;gt; '/article/{article}/edit',
    'delete-form' =&amp;gt; '/article/{article}/delete',
  ],
)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Run this command to find all PHP files that assign a non-null string to &lt;code&gt;uri_callback&lt;/code&gt; in an entity type attribute or annotation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rg 'uri_callback\s*[=:]\s*[&amp;quot;\x27]' --type php -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives are entity type class files with a named callback string. False positives include core's own attribute class constructors (which declare &lt;code&gt;uri_callback = NULL&lt;/code&gt; as a parameter default) and &lt;code&gt;EntityType.php&lt;/code&gt;; those will not match the quoted-value pattern above.&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>Remove redundant `role` attributes from HTML5 semantic landmark elements in Twig templates</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8ac1-e057-7399-9645-07c474c2352e.md</link>
      <guid isPermaLink="false">019d8ac1-e057-7399-9645-07c474c2352e</guid>
      <category>accessibility</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; BarisW, Andrew Macpherson, Mike Gifford, Liam Morland, Mike Herchel, Benjamin Mullins in &lt;a href="https://www.drupal.org/node/2655794"&gt;#2655794&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; accessibility, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTML5 semantic elements carry implicit ARIA landmark roles that all modern browsers expose automatically to assistive technologies. Adding an explicit &lt;code&gt;role&lt;/code&gt; attribute that matches the element's built-in implicit role is redundant: &lt;code&gt;role=&amp;quot;main&amp;quot;&lt;/code&gt; on &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, &lt;code&gt;role=&amp;quot;navigation&amp;quot;&lt;/code&gt; on &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;, &lt;code&gt;role=&amp;quot;complementary&amp;quot;&lt;/code&gt; on &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt;, &lt;code&gt;role=&amp;quot;banner&amp;quot;&lt;/code&gt; on &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, and &lt;code&gt;role=&amp;quot;contentinfo&amp;quot;&lt;/code&gt; on &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; are all no-ops in any browser that supports HTML5. They also trigger HTML validation warnings.&lt;/p&gt;
&lt;p&gt;These attributes were originally required for Internet Explorer 11, which did not pass implicit landmark roles to accessibility APIs. With IE11 support dropped in Drupal 10, they became dead weight. Remove the matching &lt;code&gt;role&lt;/code&gt; attribute from the element tag in Twig templates. Keep &lt;code&gt;role&lt;/code&gt; attributes when they override or supplement an element's implicit role, or when placed on non-semantic containers like &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; that have no implicit role of their own.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-twig"&gt;&amp;lt;header role=&amp;quot;banner&amp;quot;&amp;gt;
&amp;lt;main role=&amp;quot;main&amp;quot; class=&amp;quot;layout-main&amp;quot;&amp;gt;
  &amp;lt;aside class=&amp;quot;sidebar&amp;quot; role=&amp;quot;complementary&amp;quot;&amp;gt;
  &amp;lt;footer role=&amp;quot;contentinfo&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-twig"&gt;&amp;lt;header&amp;gt;
&amp;lt;main class=&amp;quot;layout-main&amp;quot;&amp;gt;
  &amp;lt;aside class=&amp;quot;sidebar&amp;quot;&amp;gt;
  &amp;lt;footer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rg -n '(&amp;lt;main[^&amp;gt;]*role=&amp;quot;main&amp;quot;|&amp;lt;nav[^&amp;gt;]*role=&amp;quot;navigation&amp;quot;|&amp;lt;aside[^&amp;gt;]*role=&amp;quot;complementary&amp;quot;|&amp;lt;header[^&amp;gt;]*role=&amp;quot;banner&amp;quot;|&amp;lt;footer[^&amp;gt;]*role=&amp;quot;contentinfo&amp;quot;)' --glob '*.twig'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each match is a true positive: the element already provides the listed implicit ARIA role. Ignore results in &lt;code&gt;stable9&lt;/code&gt; or similar legacy-compatibility themes that deliberately preserve these attributes for backward compatibility with CSS and JS selectors in downstream 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>Pop RequestStack in a `finally` block after every manual push in sub-request handlers</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8ac0-0e48-779e-a581-d151ec8bbaac.md</link>
      <guid isPermaLink="false">019d8ac0-0e48-779e-a581-d151ec8bbaac</guid>
      <category>reliability</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; cpj, larowlan, alexpott in &lt;a href="https://www.drupal.org/node/2613044"&gt;#2613044&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; reliability, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When stack middleware or a custom sub-request handler pushes a request onto &lt;code&gt;RequestStack&lt;/code&gt;, it must pop that entry in a &lt;code&gt;finally&lt;/code&gt; block after the inner kernel returns. Without a &lt;code&gt;finally&lt;/code&gt; guard, any thrown exception or early return leaves a stale entry on the stack. Stale entries cause cache contexts such as &lt;code&gt;route&lt;/code&gt; to resolve against the wrong request, which can serve poisoned cached responses to subsequent visitors.&lt;/p&gt;
&lt;p&gt;The rule: every explicit &lt;code&gt;requestStack-&amp;gt;push()&lt;/code&gt; must have a matching &lt;code&gt;requestStack-&amp;gt;pop()&lt;/code&gt; in a &lt;code&gt;finally&lt;/code&gt; block scoped to the same call. For Drupal's main request the pop can be deferred to &lt;code&gt;DrupalKernel::terminate()&lt;/code&gt; because terminate-time subscribers still need the request. For sub-requests (&lt;code&gt;$type !== MAIN_REQUEST&lt;/code&gt;) pop immediately in &lt;code&gt;finally&lt;/code&gt; after the inner &lt;code&gt;handle()&lt;/code&gt; call returns. Drupal core's &lt;code&gt;KernelPreHandle&lt;/code&gt; middleware is the authoritative example of this pattern.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
  $this-&amp;gt;drupalKernel-&amp;gt;preHandle($request);
  return $this-&amp;gt;httpKernel-&amp;gt;handle($request, $type, $catch);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
  $this-&amp;gt;drupalKernel-&amp;gt;preHandle($request);
  try {
    return $this-&amp;gt;httpKernel-&amp;gt;handle($request, $type, $catch);
  }
  finally {
    if ($type !== self::MAIN_REQUEST) {
      $this-&amp;gt;requestStack-&amp;gt;pop();
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rg -n 'requestStack-&amp;gt;push\b' --type php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Review each hit: if the &lt;code&gt;push()&lt;/code&gt; call is not followed by a &lt;code&gt;pop()&lt;/code&gt; inside a &lt;code&gt;finally&lt;/code&gt; block in the same method, it is a true positive. Test files that push a mock request for setup without a corresponding pop are likely false positives and can be skipped.&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>Add new service dependencies to plugin base classes with a nullable constructor parameter and deprecation fallback</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8a6f-7aa0-7564-a2de-18772ef1a362.md</link>
      <guid isPermaLink="false">019d8a6f-7aa0-7564-a2de-18772ef1a362</guid>
      <category>deprecation</category>
      <category>reliability</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; larowlan, alexpott, catch in &lt;a href="https://www.drupal.org/node/2571679"&gt;#2571679&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; deprecation, reliability, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a plugin base class needs a new service dependency, do not add it as a required constructor parameter immediately. Instead, append it as &lt;code&gt;?ServiceClass $service = NULL&lt;/code&gt; to &lt;code&gt;__construct()&lt;/code&gt;, fall back to &lt;code&gt;\Drupal::service()&lt;/code&gt; when it is null, and emit a &lt;code&gt;@trigger_error()&lt;/code&gt; deprecation. Always pass the real service from the container in &lt;code&gt;create()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This preserves backward compatibility for every custom or contrib module that subclasses the base class and overrides &lt;code&gt;__construct()&lt;/code&gt;. Adding a required parameter immediately breaks those subclasses at runtime with no warning. The optional nullable parameter gives downstream authors a full major-version window to update their signatures. In the next major version, remove the fallback branch and make the parameter required. Drupal core uses this pattern consistently whenever a new injectable dependency is retrofitted onto an existing plugin base class.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function __construct(
  array $configuration,
  $plugin_id,
  $plugin_definition,
  ViewExecutableFactory $executable_factory,
  EntityStorageInterface $storage,
  AccountInterface $user,
) {
  parent::__construct($configuration, $plugin_id, $plugin_definition);
}

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration, $plugin_id, $plugin_definition,
    $container-&amp;gt;get('views.executable'),
    $container-&amp;gt;get('entity_type.manager')-&amp;gt;getStorage('view'),
    $container-&amp;gt;get('current_user'),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function __construct(
  array $configuration,
  $plugin_id,
  $plugin_definition,
  ViewExecutableFactory $executable_factory,
  EntityStorageInterface $storage,
  AccountInterface $user,
  ?ContextualLinksHelper $contextual_links = NULL,
) {
  if (!$contextual_links) {
    @trigger_error('Calling ' . __METHOD__ . '() without the $contextual_links argument is deprecated in drupal:11.4.0 and will be required in drupal:13.0.0. See https://www.drupal.org/node/3382344', E_USER_DEPRECATED);
    $contextual_links = \Drupal::service(ContextualLinksHelper::class);
  }
  $this-&amp;gt;contextualLinks = $contextual_links;
  parent::__construct($configuration, $plugin_id, $plugin_definition);
}

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration, $plugin_id, $plugin_definition,
    $container-&amp;gt;get('views.executable'),
    $container-&amp;gt;get('entity_type.manager')-&amp;gt;getStorage('view'),
    $container-&amp;gt;get('current_user'),
    $container-&amp;gt;get(ContextualLinksHelper::class),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect: search for constructors that already use this pattern (or are missing it when they should have it).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;rg &amp;quot;without the \\\$.*argument is deprecated&amp;quot; --include=&amp;quot;*.php&amp;quot; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives are base-class constructors that added a new service using the nullable-with-fallback technique. False positives are essentially zero because this exact message format is used only for this pattern. When reviewing a patch that adds a required parameter to a base-class constructor without a nullable fallback, that is a missing instance of this pattern.&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>Replace deprecated `filter_formats()` and related procedural functions with `FilterFormatRepositoryInterface`</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8a6c-7a01-7698-b599-954f8861b44d.md</link>
      <guid isPermaLink="false">019d8a6c-7a01-7698-b599-954f8861b44d</guid>
      <category>deprecation</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; catch, Berdir, larowlan in &lt;a href="https://www.drupal.org/node/2536594"&gt;#2536594&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; deprecation, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Six procedural functions for working with filter formats are deprecated in Drupal 11.4 and removed in Drupal 13: &lt;code&gt;filter_formats()&lt;/code&gt;, &lt;code&gt;filter_formats_reset()&lt;/code&gt;, &lt;code&gt;filter_get_roles_by_format()&lt;/code&gt;, &lt;code&gt;filter_get_formats_by_role()&lt;/code&gt;, &lt;code&gt;filter_default_format()&lt;/code&gt;, and &lt;code&gt;filter_fallback_format()&lt;/code&gt;. Inject &lt;code&gt;FilterFormatRepositoryInterface&lt;/code&gt; instead and call the corresponding methods: &lt;code&gt;getAllFormats()&lt;/code&gt;, &lt;code&gt;getFormatsForAccount($account)&lt;/code&gt;, &lt;code&gt;getFormatsByRole($rid)&lt;/code&gt;, &lt;code&gt;getDefaultFormat($account)&lt;/code&gt;, and &lt;code&gt;getFallbackFormatId()&lt;/code&gt;. The single exception is &lt;code&gt;filter_get_roles_by_format($format)&lt;/code&gt;, which maps to &lt;code&gt;$format-&amp;gt;getRoles()&lt;/code&gt; on the entity itself. To reset the cache, invalidate the entity type list cache tags rather than calling &lt;code&gt;filter_formats_reset()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These functions relied on &lt;code&gt;drupal_static()&lt;/code&gt; and were unswappable in tests. The repository service is injectable and keyed to a stable interface. Note that &lt;code&gt;filter_formats()&lt;/code&gt; was especially error-prone: passing an &lt;code&gt;$account&lt;/code&gt; silently narrowed the result to only the formats that account could access, while omitting it returned all formats. The repository splits this into two explicitly named methods, eliminating silent behavioral divergence.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Procedural calls, deprecated in 11.4, removed in 13.0.
$all_formats = filter_formats();
$user_formats = filter_formats($account);
$default_format = filter_default_format($account);
$fallback_id = filter_fallback_format();
$roles = filter_get_roles_by_format($format);
$formats = filter_get_formats_by_role($rid);
filter_formats_reset();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Inject the service and call the repository methods.
$repo = \Drupal::service(FilterFormatRepositoryInterface::class);
$all_formats = $repo-&amp;gt;getAllFormats();
$user_formats = $repo-&amp;gt;getFormatsForAccount($account);
$default_format = $repo-&amp;gt;getDefaultFormat($account);
$fallback_id = $repo-&amp;gt;getFallbackFormatId();
$roles = $format-&amp;gt;getRoles();
$formats = $repo-&amp;gt;getFormatsByRole($rid);
\Drupal::entityTypeManager()-&amp;gt;getDefinition('filter_format')-&amp;gt;getListCacheTags();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;rg 'filter_formats\s*\(|filter_formats_reset\s*\(|filter_default_format\s*\(|filter_fallback_format\s*\(|filter_get_roles_by_format\s*\(|filter_get_formats_by_role\s*\(' --type php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives are direct call-sites in contrib or custom code. Exclude &lt;code&gt;core/modules/filter/filter.module&lt;/code&gt; (the deprecated stubs themselves) and docblock-only lines to reduce noise.&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>Declare cross-module update ordering with `hook_update_dependencies()`</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8a69-b82f-73bd-a52f-fb64f84042c5.md</link>
      <guid isPermaLink="false">019d8a69-b82f-73bd-a52f-fb64f84042c5</guid>
      <category>reliability</category>
      <category>maintainability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; hchonov, alexpott, catch, larowlan in &lt;a href="https://www.drupal.org/node/1894286"&gt;#1894286&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; reliability, maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a module's update hook depends on schema changes, config keys, or data rows introduced by a different module's update hook, declare that ordering constraint explicitly using &lt;code&gt;hook_update_dependencies()&lt;/code&gt; in the &lt;code&gt;.install&lt;/code&gt; file. Return an array entry keyed as &lt;code&gt;$dependencies['your_module'][your_update_number]&lt;/code&gt; naming the prerequisite module and update number.&lt;/p&gt;
&lt;p&gt;Drupal's update dependency graph respects only explicitly declared constraints; it does not guarantee any implicit ordering between modules. Relying on alphabetical module name, installation sequence, or assumed &amp;quot;system always runs first&amp;quot; behavior is fragile and breaks silently across Drupal version upgrades. Omitting the declaration can cause your update to run before its prerequisite, producing cryptic failures or silent data corruption. The reverse direction — declaring that your update must run &lt;em&gt;before&lt;/em&gt; another module's update — should be avoided: any site that ran the later update before your code shipped will silently ignore the constraint.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// mymodule.install
// No ordering declared; silently assumes system_update_11201 has run.
function mymodule_update_11001(): void {
  \Drupal::database()-&amp;gt;schema()-&amp;gt;addField('router', 'new_col', [...]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// mymodule.install
function mymodule_update_dependencies(): array {
  // mymodule_update_11001 must run after the router table is updated by system.
  $dependencies['mymodule'][11001] = ['system' =&amp;gt; 11201];
  return $dependencies;
}

function mymodule_update_11001(): void {
  \Drupal::database()-&amp;gt;schema()-&amp;gt;addField('router', 'new_col', [...]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect &lt;code&gt;.install&lt;/code&gt; files that have update hooks but no &lt;code&gt;_update_dependencies&lt;/code&gt; declaration — these are candidates to audit for hidden cross-module ordering assumptions:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;comm -23 \
  &amp;lt;(rg &amp;quot;function \w+_update_\d+&amp;quot; --include=&amp;quot;*.install&amp;quot; -l | sort) \
  &amp;lt;(rg &amp;quot;function \w+_update_dependencies&amp;quot; --include=&amp;quot;*.install&amp;quot; -l | sort)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives are files whose update hooks access schema, config, or data owned by another module (check for table names, config prefixes, or service calls that belong to a different module). False positives are self-contained update hooks with no cross-module data access.&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>Use `#config_target` on form elements to eliminate manual config saving in `submitForm()`</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8a66-5257-7042-b61d-1d7913995bf8.md</link>
      <guid isPermaLink="false">019d8a66-5257-7042-b61d-1d7913995bf8</guid>
      <category>maintainability</category>
      <category>reliability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; DieterHolvoet, alexpott in &lt;a href="https://www.drupal.org/node/1503146"&gt;#1503146&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; maintainability, reliability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a &lt;code&gt;ConfigFormBase&lt;/code&gt; subclass, adding &lt;code&gt;'#config_target' =&amp;gt; 'config_name:key.path'&lt;/code&gt; to a form element tells the framework to read and write that config value automatically. The form's &lt;code&gt;buildForm()&lt;/code&gt; populates the field from config, and &lt;code&gt;submitForm()&lt;/code&gt; saves it back, with no custom code required. When every field uses &lt;code&gt;#config_target&lt;/code&gt;, the &lt;code&gt;submitForm()&lt;/code&gt; override can be removed entirely and &lt;code&gt;getEditableConfigNames()&lt;/code&gt; becomes redundant (use &lt;code&gt;RedundantEditableConfigNamesTrait&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Manual &lt;code&gt;submitForm()&lt;/code&gt; overrides that fetch &lt;code&gt;$this-&amp;gt;config(...)&lt;/code&gt; and call &lt;code&gt;-&amp;gt;set(...)&lt;/code&gt; then &lt;code&gt;-&amp;gt;save()&lt;/code&gt; are error-prone boilerplate: they duplicate field names, must be kept in sync with &lt;code&gt;buildForm()&lt;/code&gt;, and often bypass the config schema validation that &lt;code&gt;#config_target&lt;/code&gt; applies automatically.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function buildForm(array $form, FormStateInterface $form_state) {
  $config = $this-&amp;gt;config('system.site');
  $form['site_name'] = [
    '#type' =&amp;gt; 'textfield',
    '#default_value' =&amp;gt; $config-&amp;gt;get('name'),
  ];
  return parent::buildForm($form, $form_state);
}

public function submitForm(array &amp;amp;$form, FormStateInterface $form_state) {
  $this-&amp;gt;config('system.site')
    -&amp;gt;set('name', $form_state-&amp;gt;getValue('site_name'))
    -&amp;gt;save();
  parent::submitForm($form, $form_state);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function buildForm(array $form, FormStateInterface $form_state) {
  $form['site_name'] = [
    '#type' =&amp;gt; 'textfield',
    '#config_target' =&amp;gt; 'system.site:name',
  ];
  return parent::buildForm($form, $form_state);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Find ConfigFormBase subclasses that still use a manual submitForm() for config saving (candidates to convert):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;rg -rl &amp;quot;extends ConfigFormBase&amp;quot; /path/to/project --glob &amp;quot;*.php&amp;quot; | xargs rg -l &amp;quot;function submitForm&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each result is a form class that overrides &lt;code&gt;submitForm()&lt;/code&gt;; inspect whether it calls &lt;code&gt;$this-&amp;gt;config(...)-&amp;gt;set(...)-&amp;gt;save()&lt;/code&gt; on values taken directly from &lt;code&gt;$form_state&lt;/code&gt;. Those are strong candidates for replacement with &lt;code&gt;#config_target&lt;/code&gt;. Classes that perform validation or side-effects in &lt;code&gt;submitForm()&lt;/code&gt; beyond pure config saving are false positives for this pattern.&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>Use service decorator with `decorates:` and `#[AutowireDecorated]` to extend a core service across module boundaries</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/patterns/019d8a66-5257-7042-b61d-1d78712b4913.md</link>
      <guid isPermaLink="false">019d8a66-5257-7042-b61d-1d78712b4913</guid>
      <category>maintainability</category>
      <category>reliability</category>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Source:&lt;/strong&gt; DieterHolvoet, alexpott in &lt;a href="https://www.drupal.org/node/1503146"&gt;#1503146&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags:&lt;/strong&gt; maintainability, reliability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a module needs to extend the behavior of a core service but cannot introduce a reverse dependency, use Symfony's service decorator pattern. Declare &lt;code&gt;decorates: original.service.id&lt;/code&gt; in your module's &lt;code&gt;services.yml&lt;/code&gt; and inject the inner service with the &lt;code&gt;#[AutowireDecorated]&lt;/code&gt; attribute in your class constructor. The container will transparently replace the original service with your decorator everywhere it is used.&lt;/p&gt;
&lt;p&gt;This avoids the coupling problems of subclassing or overriding service definitions. The decorated class implements the same interface, delegates unchanged methods to &lt;code&gt;$this-&amp;gt;decorated&lt;/code&gt;, and only extends the methods it needs to augment. Because the decorator lives in a separate module, the core module never gains a dependency on the extending module. Cache the result of expensive delegate calls when the decorated method is called frequently.&lt;/p&gt;
&lt;h2&gt;Before&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;services:
  mymodule.path_matcher:
    class: Drupal\mymodule\MyPathMatcher
    arguments: ['@path.matcher', '@path_alias.manager']
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;After&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;services:
  Drupal\mymodule\MyPathMatcher:
    public: false
    decorates: path.matcher
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detection&lt;/h2&gt;
&lt;p&gt;Detect existing decorator registrations to understand usage or find candidate services to decorate:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;rg &amp;quot;decorates:&amp;quot; /path/to/project --glob &amp;quot;*.yml&amp;quot; -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;True positives are &lt;code&gt;decorates: some.service.id&lt;/code&gt; lines in services.yml files. To find &lt;code&gt;ConfigFormBase&lt;/code&gt; subclasses that manually save config in &lt;code&gt;submitForm()&lt;/code&gt; and could migrate to &lt;code&gt;#config_target&lt;/code&gt;, see the companion pattern. False positives are rare; YAML comments containing the word &amp;quot;decorates&amp;quot; are the only likely noise.&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>