Problem/Motivation

I have a custom module with function hook_preprocess_node and use

function mymodule_preprocess_node(array &$variables) : void {
  /** @var \Drupal\node\NodeInterface $node */
  $node = $variables['node'];
  if ($node->bundle() === 'image') {
    ...
  }
}

and get the error:

Error: Call to a member function bundle() on null in mymodule_preprocess_node() (line 114 of modules/custom/mymodule/mymodule.module). 

Steps to reproduce

The node has language setting 'Not specified' (und) and fields are set to 'Not translatable' and the default language is set to 'en'. More languages exist in the site (de, fr, ...).

With drupal 11.2.10 all works well. Updating to drupal 11.3.2 throws the error.

No other changes are made.

Comments

walterp created an issue. See original summary.

cilefen’s picture

Component: node system » theme system

What is the content of $variables in this case?

walterp’s picture

Component: theme system » node system

I do not understand the question.
$variables is from 'function mymodule_preprocess_node(array &$variables) : void'

cilefen’s picture

Component: node system » theme system

What are the contents of the $variables array when node is null within it?

This is a theme hook so let’s leave this issue in the theme system for now, unless you have identified the Core commit that changed the hook’s behavior and it’s in the Node system.

walterp’s picture

Component: theme system » node system

drupal 11.3.x:
If I remove the $node = $variables['node']; in my module_preprocess_node, I get this error:
Error: Call to a member function language() on null in Drupal\locale\Hook\LocaleThemeHooks->preprocessNode() (line 99 of core/modules/locale/src/Hook/LocaleThemeHooks.php).

drupal 11.2.10: output of dpm($variables); in mymodule_preprocess_node

$variables array (15)
    elements => array (66)
    theme_hook_original => string (4) "node"
    attributes => array (1)
    title_attributes => array (0)
    content_attributes => array (0)
    title_prefix => array (0)
    title_suffix => array (1)
    db_is_active => boolean true
    is_admin => boolean true
    logged_in => boolean true
    user => Drupal\Core\Session\AccountProxy#5305 (5)
    directory => string (29) "themes/custom/interelectronix"
    #cache => array (1)
    language => string (2) "en"
    language_direction => string (3) "ltr"
cilefen’s picture

Component: node system » theme system

What is that output in 11.3.x? This is what I add am asking for.

walterp’s picture

Component: theme system » node system

In drupal 11.3.x I do not get an output for dpm($variables);. As described before, I get this error:

Error: Call to a member function language() on null in Drupal\locale\Hook\LocaleThemeHooks->preprocessNode() (line 99 of core/modules/locale/src/Hook/LocaleThemeHooks.php).

nicxvan’s picture

Version: 11.3.x-dev » main
Component: node system » theme system

Do you have any other custom modules with a hook_preprocess_node?

Can you post which contrib modules you have enabled?

@walterp please stop changing the component to the node system, this issue is not the node system it is a theme system issue.

walterp’s picture

Version: main » 11.3.x-dev

I use hook_preprocess_node in 3 custom modules and in my custom theme.

Installed and enabled contrib modules:
action
address
admin_toolbar
admin_toolbar_tools
avif
bamboo_twig
bamboo_twig_config
bamboo_twig_i18n
bamboo_twig_loader
bamboo_twig_path
block_class
cloudflare
convert_bundles
cookies
critical_css
csp
csv_serialization
ctools
ctools_entity_mask
devel
diff
ds
entity_clone
entity_reference_revisions
extra_field
fences
field_group
field_permissions
field_token_value
file_mdm
file_mdm_exif
file_mdm_font
file_to_media
gin_toolbar
google_analytics
image_effects
image_style_quality
image_style_warmer
imagemagick
imce
inline_entity_form
jquery_ui
jquery_ui_datepicker
jquery_ui_tabs
jquery_ui_tooltip
key
kint
klaro
language_switcher_extended
lazy
linkit
manage_display
markdown_easy
metatag
metatag_hreflang
metatag_views
minifyhtml
node_read_time
node_revision_delete
paragraph_view_mode
paragraphs
paragraphs_table
pathauto
pathologic
potx
queue_ui
redirect_404
redirect
replicate
replicate_ui
restui
schema_metatag
search_api_db
search_api
search_api_block
search_api_exclude_entity
smtp
sophron
taxonomy_term_depth
tmgmt_content
tmgmt_locale
tmgmt_config
tmgmt
tmgmt_file
tmgmt_language_combination
tmgmt_local
tmgmt_deepl_glossary
tmgmt_deepl
tmgmt_microsoft
toc_js
token
translation_views
twig_tools
twig_tweak
twig_vardumper
ultimate_cron
upgrade_status
views_bulk_operations
views_data_export
views_fields_on_off
views_infinite_scroll
views_term_hierarchy_weight_field
watchdog_prune
webform_node
webform_templates
webform_ui
webform
webform_spam_words
xmlsitemap
xmlsitemap_custom

cilefen’s picture

Version: 11.3.x-dev » main

@walterp It is necessary that you debug $variables above the line on which the error occurs.

walterp’s picture

Version: main » 11.3.x-dev

dpm($variables) is the first line in my preprocess_node function - like this:

function mymodule_preprocess_node(array &$variables) : void {
  dpm($variables);
  // $node = $variables['node'];
}

And I remove all other code from my preprocess_node function.

nicxvan’s picture

Version: 11.3.x-dev » main

Thank you @walterp, two more requests if you can!

1. Can you find all instances of hook_preprocess_node in these? If you search for preprocess_node that should find all OOP and functions in your codebase. Your theme may also have one of these hooks, this will help us figure out where the node variable is being dropped. We only need to know which modules have implementations, you don't need to copy the whole function or method.

2. This belongs on main, that is where the bug will be fixed if there is one, it would then be backported to 11.3 if it is fixed in time. It's a new process, but it is where this issue belongs.

I suspect there is a prepreprocess node hook running earlier that is unsetting the node key on $variables.

walterp’s picture

Version: main » 11.3.x-dev

I have 3 custom modules with hook_preprocess_node and also one in my theme.

  1. function ix_helper_preprocess_node(&$variables) : void {
  2. function ix_paragraph_rows_preprocess_node(array &$variables) : void {
  3. function ix_vimeo_preprocess_node(&$variables) : void {
  4. function theme_preprocess_node(&$variables) : void {

The functions are called in this order.

Please keep in mind, that I get this errors only on a special content type, where the nodes have language setting 'Not specified' (und) and fields are set to 'Not translatable' and the default language is set to 'en'. More languages exist in the site (de, fr, ...).

If I call a content type, where the nodes have language setting = 'en' and are translated, the error do not occur.

cilefen’s picture

@walterp You will have to halt execution to see the values, using something besides dpm. I'm sorry that wasn't clear. dpm is limited by not being able to run through errors or exceptions. Alternatively, you could use a real debugger, like Xdebug, and set a breakpoint on that line.

walterp’s picture

@cilefen I setup XDebug and set a breakpoint for errors generally and a breakpoint before the line
Error: Call to a member function bundle() on null in mymodule_preprocess_node() (line 114 of modules/custom/mymodule/mymodule.module).
where the error occur.

In the Locals I get

$node = uninitialized
$variables = array(15)
  elements = array()66
  theme_hook_original = "node"
  attributes = array(1)
  title_attributes = array(0)
  content_attributes = array(0)
  title_prefix = array(0)
  title_suffix = array(1)
  db_is_active = true
  is_admin = true
  logged_in = true
  user = Drupal\Core\Session\AccountProxy
  directory = "themes/custom/interelectronix
  #cache = array(1)
  language = "en"
  language_direction = "ltr"

No $variables['node'] available.

But also no error thrown before.

nicxvan’s picture

Yeah, we need to find out which hook is running before the locale one that is dropping the node variable.

Typically they are called in alphabetical order which means it's likely one of the ones before the L's in your list.

I suspect there is an unset($variables['node'])

walterp’s picture

In my code, I use $variables['node'] 3 times - and nowhere unset($variables['node']).

Where is $variables['node'] created the first time? Perhaps it's not created, as language is 'en' and the content type has language 'und'?

nicxvan’s picture

That is a good question, it is set in NodeThemeHooks.php preprocessNode(&$variables)

cilefen’s picture

@walterp, can you get the call stack when at that breakpoint? That will, I hope, show the prior hook function invocations.

walterp’s picture

I set a breakpoint in NodeThemeHooks.php (preprocessNode) at the end of the function and one in my module, which causes the first error.

If I call a node content type, which causes no error, XDebug stops at the breakpoint in NodeThemeHooks and $variables['node'] is set.

If I call the node content type, which causes the error (language = 'Not specified'), the breakpoint in NodeThemeHooks is not used and XDebug stops at the second breakpoint in my custom module and no $variables['node'] is set.

In my opinion, the crux of the matter is/are the language handling?!

cilefen’s picture

Can you examine the call stack when stopped at those breakpoints?

nicxvan’s picture

This is a tricky one! I appreciate your willingness to continue debugging, as @cilefen said full call stacks there would be helpful.

Other than language is there anything else special about that content type? Was it defined normally through the ui and config management?

walterp’s picture

@cilefen Call stack in for my preprocess_node function:

ix_paragraph_rows_preprocess_node
Drupal\Core\Extension\ModuleHandler->invoke
Drupal\Core\Theme\ThemeManager->{closure:/var/www/html/web/core/lib/Drupal/Core/Theme/ThemeManager.php:293-319}
Drupal\Core\ThemeManager->render
Drupal\Core\Render\Renderer->doRender
Drupal\Core\Render\Renderer->render
Drupal\Core\Render\MainContent\HtmlRenderer->{closure:/var/www/html/web/core/lib/Drupal/Core/MainContent\HtmlRenderer.php:235-243}
Drupal\Core\Render\Renderer::{closure:/var/www/html/web/core/lib/Drupal/Core/Render\Renderer.php:634-634}
{fiber:FFFF8A21A640

@nicxvan I think - probably - the content type settings are defined in the UI, but goes back to drupal 7.
Special thing could be, that there are nodes of this content type, which are translated (language en + de, fr, ...) and nodes, which are not translated (language 'Not specified').

cilefen’s picture

A next step would be to set a breakpoint in Drupal\Core\Extension\ModuleHandler->invoke and watch how $variables, which may have a different name inside invoke, changes as each hook executes. This will provide insight into when node is removed.

walterp’s picture

Since I am not very experienced with debugging, I would be grateful for more detailed instructions. But I think, I found the place to set the breakpoint:
file: web/core/lib/Drupal/Core/Extension/ModuleHandler.php

  public function invoke($module, $hook, array $args = []) {
    $list = $this->getHookImplementationList($hook);
    $listeners = $list->getForModule($module);
    if ($listeners) {

The first break shows:

$args = array(3)
  0 = array(14)
  1 = "entity_page_title
  ...

The second break shows:

$args = array(3)
  0 = array(19)
  1 = "node"
  ...

If I call a working content type, 0 = array(...) - which I think corresponds with $variables - contain a line with node = Drupal\node\EntityNode.

If I call a not working content type, there is no node = Drupal\node\EntityNode in this 0 = array().

I hope, this is the information @cilefen you want to have. So the "node" information is never included.

cilefen’s picture

Believe it or not, there isn't much more to debuggers, so you are getting experience right now!

Yes, you are in the right place. You want to check the $hook value where you put your breakpoint to be sure you are only looking at the arguments for hook_preprocess_node, because every hook invocation will pass through that function.

If that's frustrating because of the large number of callers, you could move your breakpoint up the call stack into ThemeManager, around line 293, where the preprocessor hooks are determined. There you will see only theme-related hook invocations.

cilefen’s picture

This is more to remind myself: we are looking for invocations of hook_preprocess_node when the node key is missing from the $variables array, and then to determine what removed node.

walterp’s picture

There are only 4 times, the breakpoint in function invoke() appears, before the error on missing $node = $variables['node'] is thrown:

1. run

$args = array(3)
  0 = array(14)
  1 = "entity_page_title"
$hook = "preprocess"
$module = "contextual"
  

2. run

$args = array(3)
  0 = array(12)
  1 = "node"
$hook = "preprocess"
$module = "contextual"
  

3. run

$args = array(3)
  0 = array(13)
  1 = "node"
$hook = "preprocess_node"
$module = "ix_helper"

4. run
<code>$args = array(3)
  0 = array(13)
  1 = "node"
$hook = "preprocess_node"
$module = "ix_paragraph_rows"
  

After that the error is thrown. In none of the $args['0'] arrays is a $args['0']['node'] available.

nicxvan’s picture

Can you try the fix in: https://www.drupal.org/project/drupal/issues/3556794

Minor correction to 26 theme preprocess don't run through module handler. They go through themeManager.

walterp’s picture

@nicxvan: I applied the patch, but unfortunately the same behaviour/error.

cilefen’s picture

@nicxvan The stack track says otherwise. Is that the problem here?

nicxvan’s picture

It is possible, but I think the preprocess function is a red herring, it's a symptom not the cause.

Something is preventing that content type from being identified as a node.

quietone’s picture

Version: 11.3.x-dev » main

@walterp, Issues for Drupal core should be targeted to the 'main' branch, our primary development branch. Changes are made on the main branch first, and are then back ported as needed according to the Core change policies. The version the problem was discovered on should be stated in the issue summary Problem/Motivation section. Thanks.

walterp’s picture

The problem persists. Are there any ideas on how to find the cause?

walterp’s picture

I still can't update to Drupal 11.3.x. Are there any suggestions on how to resolve this issue?

walterp’s picture

After much research, it seems I have found the solution.

  1. In Drupal 11.3, node preprocessing moved to initial preprocess (NodeThemeHooks::preprocessNode) in NodeThemeHooks.php (line 43).
  2. template_preprocess_node() is now deprecated in node.module (line 272).
  3. My registry alter in ix_datasheet.module (line 60) (and similarly in ix_paragraph_rows.module (line 74)) copies preprocess functions from base hook, but not initial preprocess.
  4. Result: for those custom node suggestions, core node hydration may not run, $variables['node'] can be null, then crash at ix_paragraph_rows.module (line 113).

I changed:

function ix_datasheet_theme_registry_alter(&$theme_registry) : void {
  $extension   = '.html.twig';
  $module_path = \Drupal::service('extension.path.resolver')->getPath('module', 'ix_datasheet');
  $files = \Drupal::service('file_system')->scanDirectory($module_path . '/templates', '/' . preg_quote($extension) . '$/', [
    'key' => 'filename',
    'recurse' => FALSE,
  ]);

  $fs = \Drupal::service('file_system');

  foreach ($files as $file) {
    $template = $fs->basename($file->filename, $extension);
    $theme    = str_replace('-', '_', $template);
    $__expt = explode('__', $theme, 2);
    $base_theme = empty($__expt[0]) ? null : $__expt[0];
    $specific = empty($__expt[1]) ? null : $__expt[1];
    if (!empty($specific) && isset($theme_registry[$base_theme])) {
      $theme_info = array(
        'template'   => $template,
        'path'       => $fs->dirname($file->uri),
        'render element'  => $theme_registry[$base_theme]['render element'],
        'base hook'  => $base_theme,
        'type'       => 'module',
        'theme path' => $module_path,
        'preprocess functions' => $theme_registry[$base_theme]['preprocess functions'],
      );

      $theme_registry[$theme] = $theme_info;
    }
  }
}

to this:

function ix_datasheet_theme_registry_alter(&$theme_registry) : void {
  $extension   = '.html.twig';
  $module_path = \Drupal::service('extension.path.resolver')->getPath('module', 'ix_datasheet');
  $files = \Drupal::service('file_system')->scanDirectory($module_path . '/templates', '/' . preg_quote($extension) . '$/', [
    'key' => 'filename',
    'recurse' => FALSE,
  ]);

  $fs = \Drupal::service('file_system');

  foreach ($files as $file) {
    $template = $fs->basename($file->filename, $extension);
    $theme    = str_replace('-', '_', $template);
    $__expt = explode('__', $theme, 2);
    $base_theme = empty($__expt[0]) ? null : $__expt[0];
    $specific = empty($__expt[1]) ? null : $__expt[1];
    if (!empty($specific) && isset($theme_registry[$base_theme])) {
      $base_info = $theme_registry[$base_theme];
      $theme_info = $base_info;
      $theme_info['template'] = $template;
      $theme_info['path'] = $fs->dirname($file->uri);
      $theme_info['base hook'] = $base_theme;
      $theme_info['type'] = 'module';
      $theme_info['theme path'] = $module_path;

      $theme_registry[$theme] = $theme_info;
    }
  }
}
nicxvan’s picture

You're doing automatic suggestion discovery manually in a theme registry alter in a module?

Why not just use hook_theme?

nicxvan’s picture

Category: Bug report » Support request
Status: Active » Closed (works as designed)

I would recommend reviewing why template are being included like this and using hook_theme as intended for modules, you'll gain a lot in terms of performance and predictability most likely as well.

Here are a couple of links with more information on the initial preprocess:
https://www.drupal.org/node/3549500
https://www.drupal.org/node/3504125

I appreciate you sticking through this, and I'm glad you found a solution.

I'm going to close this now as works as designed and a support request and apply credit to everyone in the issue.

Feel free to reply if you have any questions, but slack is generally better for support requests.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.