Some theme hooks are not invoking (depends on templates order provided by filesystem)

Created on 17 March 2022, almost 3 years ago
Updated 6 June 2024, 7 months ago

Problem/Motivation

Is some cases theme hook for default view mode is not invoked when rendering entity in some non-default view mode.
Basically problem can be with any theme hook with some suggestion (double underscores after base hook).

example:
my_theme_preprocess_paragraph__my_paragraph()
is not invoked when rendering this paragraph in teaser view mode.
Only related theme hook my_theme_preprocess_paragraph__my_paragraph__teaser() is invoked.

After debugging I realised that problem is cased by theme hooks order, provided by filesystem.

If your filesystem returns template
paragraph--my-paragraph--teaser.html.twig
before
paragraph--my-paragraph.html.twig
preprocess function for default view mode is not listed in related hook preprocess functions in
web/core/lib/Drupal/Core/Theme/Registry.php

It happens here (in that case 'incomplete preprocess functions' is removed which cause missing preprocess function for default view mode in teaser hook data):

/**
   * Completes the definition of the requested suggestion hook.
   *
   * @param string $hook
   *   The name of the suggestion hook to complete.
   * @param array $cache
   *   The theme registry, as documented in
   *   \Drupal\Core\Theme\Registry::processExtension().
   */
  protected function completeSuggestion($hook, array &$cache) {
    $previous_hook = $hook;
    $incomplete_previous_hook = [];
    // Continue looping if the candidate hook doesn't exist or if the candidate
    // hook has incomplete preprocess functions, and if the candidate hook is a
    // suggestion (has a double underscore).
    while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions']))
      && $pos = strrpos($previous_hook, '__')) {
      // Find the first existing candidate hook that has incomplete preprocess
      // functions.
      if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) {
        $incomplete_previous_hook = $cache[$previous_hook];
        unset($incomplete_previous_hook['incomplete preprocess functions']);
      }
      $previous_hook = substr($previous_hook, 0, $pos);
      $this->mergePreprocessFunctions($hook, $previous_hook, $incomplete_previous_hook, $cache);
    }

    // In addition to processing suggestions, include base hooks.
    if (isset($cache[$hook]['base hook'])) {
      // In order to retain the additions from above, pass in the current hook
      // as the parent hook, otherwise it will be overwritten.
      $this->mergePreprocessFunctions($hook, $cache[$hook]['base hook'], $cache[$hook], $cache);
    }
  }

Theme hooks order comes from twig_theme() function in web/core/themes/engines/twig/twig.engine

/**
 * Implements hook_theme().
 */
function twig_theme($existing, $type, $theme, $path) {
  $templates = drupal_find_theme_functions($existing, [$theme]);
  $templates += drupal_find_theme_templates($existing, '.html.twig', $path);
  return $templates;
}

List of templates is generated here in drupal_find_theme_templates()

$files = \Drupal::service('file_system')->scanDirectory($path, $regex, ['key' => 'filename']);

In code of this service is called readdir() function and the entries are returned in the order in which they are stored by the filesystem (according php documentation).

Steps to reproduce

* Create new paragraph type My paragraph (machine name my_paragraph)
* Allow this paragraph in some entity reference field and set for this field in display settings 'Rendered entity' and teaser view mode
* Create two preprocess function in your theme: my_theme_preprocess_paragraph__my_paragraph() and my_theme_preprocess_paragraph__my_paragraph__teaser()
* Create related templates in your theme: paragraph--my-paragraph.html.twig and paragraph--my-paragraph--teaser.html.twig

* Because order of templates (related theme hooks) depends on filesystem, it may vary on every environment. If you make temporary hardcode change here, you can reproduce that behavior:
in web/core/themes/engines/twig/twig.engine

function twig_theme($existing, $type, $theme, $path) {
  $templates = drupal_find_theme_functions($existing, [$theme]);
  $templates += drupal_find_theme_templates($existing, '.html.twig', $path);
  if ($theme === 'my_theme') {
    $myParagraphDefaultViewModeHook = $templates['paragraph__my_paragraph'];
    unset($templates['paragraph__my_paragraph']);
    // move that theme hook to the end of array
    // simulate situation when filesystem sorts templates wrong way
    // (first paragraph__my_paragraph__teaser then paragraph__my_paragraph)
    $templates['paragraph__my_paragraph'] = $myParagraphDefaultViewModeHook;
  }
  return $templates;
}

* Clear cache
* Create some content with this paragraph and open that page.

Result: my_theme_preprocess_paragraph__my_paragraph__teaser() is invoked, my_theme_preprocess_paragraph__my_paragraph() isn't.

Proposed resolution

Sort returned theme hooks from twig_theme().
I have no experiences with contributing so please, can anybody check this a see my patch?

Remaining tasks

  1. โœ… Write the fix
  2. โŒ Write test: readdir() has a built-in random behavior: The returned values are the same, but the order of the values can be random, which affects order of templates. So this doesn't seem reliably reproducible.
๐Ÿ› Bug report
Status

Needs work

Version

11.0 ๐Ÿ”ฅ

Component
Themeย  โ†’

Last updated 4 days ago

Created by

๐Ÿ‡จ๐Ÿ‡ฟCzech Republic chOdec

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

  • Needs issue summary update

    Issue summaries save everyone time if they are kept up-to-date. See Update issue summary task instructions.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom rossb89 Bristol

    I have just encountered this very issue this morning and couldn't figure out what the problem was intially.

    My issue turned out to be the same as you have detailed in the original post:

    If I have the following preprocess function: THEME_preprocess_field__paragraph__field_name()
    and no template for field--paragraph--field-name
    but do have a template for field--paragraph--field-name--bundle
    the 'non bundle' (less specific) preprocess function will be available in the theme registry and apply to the more specific template.

    If you do have a template as well for field--paragraph--field-name in addition to field--paragraph--field-name--bundle the theme registry then doesn't then have the 'non bundle' (more generic) preprocessor THEME_preprocess_field__paragraph__field_name() available.

    The ksort in the patch does indeed fix the issues found and cause the order to be sorted and consistent.

    Although @mdupont has a valid point here in that maybe we should be adding an additional key the options param of FileSystem::scanDirectory()...

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia nterbogt

    Re-rolling patch for 10.1.x.

  • last update about 1 year ago
    30,413 pass
  • last update about 1 year ago
    29,672 pass
  • ๐Ÿ‡ฆ๐Ÿ‡นAustria hudri Austria

    Wow, kudos to @chOdec for finding this bug.

    This bug is a worst-case scenario for DX, because it introduces a random and non-deterministic behavior in template and hook processing.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia kim.pepper ๐Ÿ„โ€โ™‚๏ธ๐Ÿ‡ฆ๐Ÿ‡บSydney, Australia

    Given the patch #8 is fixing the issue in twig_theme() I'm moving this back to theme queue.

  • Status changed to Needs review 9 months ago
  • ๐Ÿ‡ฆ๐Ÿ‡นAustria hudri Austria
  • Status changed to Needs work 9 months ago
  • The Needs Review Queue Bot โ†’ tested this issue.

    While you are making the above changes, we recommend that you convert this patch to a merge request โ†’ . Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bnjmnm Ann Arbor, MI

    This needs tests
    It would be helpful to get the issue summary updated to provide a scenario that doesn't use paragraphs hooks as the example so this is clearly a core issue. Even better - as part of creating the tests, make a test -only theme that reproduces the reported problem, yet works properly once the fix in #8 is present. If we have a test that fails without #8 and passes with it, I'm happy to commit.

    It's probably worth checking a bit to see if the ksort is sufficient. In all cases does the specificity order match php sort order?

    (and tbh even if it doesn't match 1:1, as long as it doesn't break anything, the ksort fix should happen and the edge cases figured out later).

  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia yash.rode pune

    I was able to reproduce the issue with Paragraphs, but as we cannot write test for paragraph I was trying to reproduce the same thing for Custom block but unfortunately I am unable to do so. can someone take a look at this?

  • ๐Ÿ‡ฆ๐Ÿ‡นAustria hudri Austria

    I don't know if this bug can be reliably reproduced and test-cased because of

    In code of this service is called readdir() function and the entries are returned in the order in which they are stored by the filesystem (according php documentation).

    and

    * Because order of templates (related theme hooks) depends on filesystem, it may vary on every environment.

    Basically PHP's readdir() has a built-in random behavior.

    Given the unintrusive nature of this patch, can we add this without tests?

  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia yash.rode pune
  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia yash.rode pune
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bnjmnm Ann Arbor, MI

    This problem can be reproduced regardless of filesystem, and tests are important not just to ensure this does not cause problems, but to ensure future changes do not un-fix whatever is addressed here.

    While it is true that the order of readdir() results returned by a single call can differ depending on the filesystem the \Drupal\Core\File\FileSystem::doScanDirectory
    method calls readdir() recursively per directory. The recursive calls to

     do occur in a predictable order (parent before child) and returns an array with the subdirectory results appearing before the parent results 
     <code>return array_merge(array_merge(...$files_in_sub_dirs), $files_in_this_directory); 

    If you have a template file you consistently want to appear before the others in the returned array, put it in a subdirectory of a directory including templates it should appear before.

    In other words, this can be tested by adding a custom module or theme that provides templates with a directory structure where the template that should appear first is in a subdirectory of the directory with the templates it should appear after.

    it may also be worth seeing if this is better to address in the logic of completeSuggestion since higher-priority theme suggestions do not necessarily correlate with alphabetical order.

  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia yash.rode pune

    Hi @bnjmnm, I tried the steps from #3270083-18: Some theme hooks are not invoking (depends on templates order provided by filesystem) โ†’

    In other words, this can be tested by adding a custom module or theme that provides templates with a directory structure where the template that should appear first is in a subdirectory of the directory with the templates it should appear after.


    But it was loading

    Main Directory Template

    instead of subdirectory one.

    if return array_merge(array_merge(...$files_in_sub_dirs), $files_in_this_directory); is reversed i.e return array_merge(, $files_in_this_directory, array_merge(...$files_in_sub_dirs)); , then it will load the sub directory one.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bnjmnm Ann Arbor, MI

    @yash.rode if there are two files with identical names then that is what would happen. Drupal doesn't support identically named templates in a theme so that doesn't have to be tested.

  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia prashant.c Dharamshala

    Prashant.c โ†’ made their first commit to this issueโ€™s fork.

  • Merge request !8307ksort for theme templates โ†’ (Open) created by prashant.c
  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia prashant.c Dharamshala

    Raised a PR using the patch from https://www.drupal.org/project/drupal/issues/3270083#comment-15262719 ๐Ÿ› Some theme hooks are not invoking (depends on templates order provided by filesystem) Needs work .

    However, in this, we are sorting the template array in ascending order by key names.
    Will this order will impact any rendering for example region templates applied after block, fields, and nodes?

  • Pipeline finished with Failed
    7 months ago
    Total: 533s
    #192359
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bnjmnm Ann Arbor, MI

    However, in this, we are sorting the template array in ascending order by key names.
    Will this order will impact any rendering for example region templates applied after block, fields, and nodes?

    I think this is a reasonable concern and I was hoping the tests added would make this more evident. Sounds like you noticed the same thing and didn't need tests to see it ๐Ÿ‘. The current solution clearly fixes it for some use cases, but alphabetical order is not necessarily the order of template priority. For example, if we take this logically ordered list of suggestions from a real site:

    <!-- FILE NAME SUGGESTIONS:
       * field--node--body--article.html.twig
       * field--node--body.html.twig
       * field--node--article.html.twig
       * field--body.html.twig
       x field--text-with-summary.html.twig
       * field.html.twig
    -->

    Sorting it alphabetically changes it to an order we don't want

    Array
    (
        [0] => field--body.html.twig
        [1] => field--node--article.html.twig
        [2] => field--node--body--article.html.twig
        [3] => field--node--body.html.twig
        [4] => field--text-with-summary.html.twig
        [5] => field.html.twig
    )

    It is probably better if the solution focuses on the logic in \Drupal\Core\Theme\Registry::completeSuggestion, and make it so the logic is consistent regardless of the array order as neither filesystem order or alphabetical sorting reflect the suggestion priority.

    But since this will need tests to land anyway, those can exist before looking into how to solve this in \Drupal\Core\Theme\Registry::completeSuggestion.

Production build 0.71.5 2024