Problem/Motivation

I'm using field_group on one of my content types. Today I needed to add a new base field (boolean) on the same content type, and the field needs to be in the form display. I didn't want to set a weight, as I'm expecting Drupal will add one for me, by calculating the EntityFormDisplay::getHighestWeight().

There is an infinite loop in this logic when Drupal try to rebuild the form display - by visiting the content type Manage form display page with an empty cache - and field_group is part of the process.

I'm raising this a Critical because the Manage form display page becomes unresponsive.

Below the xdebug error stack:
Fatal error: Maximum function nesting level of '2048' reached, aborting!

[...]
19	Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm->getEntityFromRouteMatch( )	.../HtmlEntityFormController.php:73
20	Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm->getEntityDisplay( )	.../EntityDisplayFormBase.php:70
21	entity_get_form_display( )	.../EntityFormDisplayEditForm.php:56
22	Drupal\Core\Entity\Entity::load( )	.../entity.inc:558
23	Drupal\Core\Config\Entity\ConfigEntityStorage->load( )	.../Entity.php:524
24	Drupal\Core\Config\Entity\ConfigEntityStorage->loadMultiple( )	.../EntityStorageBase.php:212
25	Drupal\Core\Config\Entity\ConfigEntityStorage->doLoadMultiple( )	.../EntityStorageBase.php:242
26	Drupal\Core\Config\Entity\ConfigEntityStorage->mapFromStorageRecords( )	.../ConfigEntityStorage.php:189
27	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->__construct( )	.../EntityStorageBase.php:322
28	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->__construct( )	.../EntityFormDisplay.php:129
29	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->init( )	.../EntityDisplayBase.php:143
30	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->init( )	.../FieldLayoutEntityDisplayTrait.php:107
31	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->setComponent( )	.../EntityDisplayBase.php:197
32	Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay->getHighestWeight( )	.../EntityDisplayBase.php:365
33	Drupal\Core\Extension\ModuleHandler->invokeAll( )	.../EntityDisplayBase.php:409
34	call_user_func_array:{/var/www/html/web/core/lib/Drupal/Core/Extension/ModuleHandler.php:402} ( )	.../ModuleHandler.php:402
35	<strong>field_group_field_info_max_weight( )	</strong>.../ModuleHandler.php:402
36	<strong>field_group_info_groups( )	</strong>.../field_group.module:142
37	Drupal\Core\Entity\Entity::load( )
[...]

At this point the loop starts, field_group_info_groups() has called EntityFormDisplay::load() in 37, which is what actually started the process on 21/22.

How to reproduce:

  1. Add field_group to your content type (although by reading the code it looks like having field_group module enable is enough to trigger the bug.)
  2. Add a base field to your content type with setDisplayConfigurable('form', TRUE), w/o specifing a 'weight' in setDisplayOptions()
  3. flush the cache
  4. Visit your content type Manage form display.
  5. You either see the blank page of dead, or any kind of php failure or - with xdebug enabled - "Fatal error: Maximum function nesting level of 'xxx' reached, aborting!"

Proposed resolution

I'm not sure what the solution can be in here. It looks like field_group is doing as expected, and core doesn't give much information from where to pull the weight on EntityFormDisplay::getHighestWeight() other than from the display itself.
But if this is being rebuilt then an infinite loop is created.

Remaining tasks

Figure it out what to do, then fix it.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

gambry created an issue. See original summary.

gambry’s picture

As mentioned in the issue summary, the only workaround is specifyng a 'weight' in setDisplayOptions():

BaseFieldDefinition::create('boolean')
    ->setLabel(t('My Field label'))
    ->setDescription(t('Check this box to do something.'))
    ->setDefaultValue(FALSE)
    ->setInitialValue(FALSE)
    ->setDisplayOptions('form', array(
      'type' => 'boolean_checkbox',
      'settings' => array(
        'display_label' => TRUE,
      ),
      'weight' => 99,
    ))
    ->setDisplayConfigurable('form', TRUE);
seanr’s picture

This also fatals in a drush cim -y that enables field_group as part of the imported config when a module has defined other fields.

gena.io’s picture

I've also got this error, and, if we disable xdebug from php.ini, we will receive "PHP Fatal error: Maximum execution time of %number seconds exceeded" with the following PHP crushing.

gena.io’s picture

Status: Active » Needs work
gena.io’s picture

Status: Needs work » Needs review

Few ways how to fix it:

1) Check if your updates through update.php/drush updb have been finished correctly, especially if system_post_update_extra_fields() has executed. If it hasn't, try to re-run your updates and pay attention to errors if they appear.
2) Check your hook_entity_extra_field_info() for weight parameter existence. In my case, it was absent and caused the mentioned bug. Your hook_entity_extra_field_info() should look like this:

function test_entity_extra_field_info() {
   $extra['node']['test_type']['display']['test'] = [
     'label' => t('Test label'),
     'weight' => 0,
   ];
 
   return $extra;

Hope it will help you.

charginghawk’s picture

I ran into this issue in a roundabout way - while saving a node with the menu settings populated, the new menu item was run through Lingotek, which loaded view display and triggered field_group_field_info_max_weight() and set off the infinite loop (and subsequently caused any node save to fail if that field was populated). I've got a patch that checks for recursion and shuts it down based on similar work here:

https://www.drupal.org/project/drupal/issues/2966137

Status: Needs review » Needs work

The last submitted patch, 7: 2951175-7-recursion-check.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

charginghawk’s picture

Status: Needs work » Needs review

Marking needs review since the failure was from the branch, not the patch.

joachim’s picture

Status: Needs review » Needs work

I'm not sure this is the right way to fix this.

From what I can tell, the problem happens because of this:

- 1. when an entity display config entity (ie view or form configuration) is loaded, it sets up all of its components.
- 2. if a component doesn't specify a weight, then it puts it at the bottom:

    // If no weight specified, make sure the field sinks at the bottom.
    if (!isset($options['weight'])) {
      $max = $this->getHighestWeight();
      $options['weight'] = isset($max) ? $max + 1 : 0;
    }

3. getHighestWeight() checks all the components' weights. As part of that, it invokes hook_field_info_max_weight() to let other modules act.
4. field_group's implementation of this, field_group_field_info_max_weight(), calls field_group_info_groups() to load all the groups
5. in order to load the groups, it loads up the display entity, EntityFormDisplay::load/EntityViewDisplay::load
6. and BOOM we're back at step 1

I think the problem is that field_group_field_info_max_weight() needs the display config entity in order to act. Arguably, hook_field_info_max_weight() should be providing it as a parameter -- which would be something to fix in core.

charginghawk’s picture

IMO, infinite recursion under any circumstances isn't acceptable, and this fixes that in a definite way without losing anything. It'd just be cruft if a theoretical core solution comes along, but in that case, we'll be updating this code anyway and can axe it then. In the meantime, this solves a critical issue for a heavily used module. That said, the patch is there for anyone who wants it.

gambry’s picture

I see @joachim , I believe Core should either suggest a solution or at least alert developers. But doing this could seriously take time... and core committers may also argue this is something field_group should take care of, and in that case we just wasted time.

I'd +1 #11 as this is critical, however currently field_group_field_info_max_weight is returning NULL when a recursion is triggered. This is not ideal if actually there is a field_group with a valid weight?
Why instead of returning NULL don't we read directly from the config entity?

dionsj’s picture

I've encountered this issue on version 1.x of field_group, I made the same changes as the patch in #7, but for version 1.x.
It solves the problem for me.

I've attached the patch for anyone who is also still using 1.x

super_romeo’s picture

@gambry's question from #12 is important.

swentel’s picture

Why instead of returning NULL don't we read directly from the config entity?

That was my initial thought as well, but not sure whether that is worth the effort. Will talk with alex whether if that's a good idea or not.

swentel’s picture

Status: Needs work » Reviewed & tested by the community

Talked with some other people and this approach is fine, so will commit and push. Made a tiny change in the comments: used 'entity display' instead of 'entity' alone.

I'd +1 #11 as this is critical, however currently field_group_field_info_max_weight is returning NULL when a recursion is triggered. This is not ideal if actually there is a field_group with a valid weight?

This is actually fine, NULL is returned for the second time this hooks comes in, but that doesn't matter. When it can finally go through, it returns the max weight fine - unless I saw it wrong in my debugger, but even then I don't mind /that/ much.

  • swentel committed 997e868 on 8.x-3.x authored by dion-jensen
    Issue #2951175 by charginghawk, dion-jensen: Infinite recursion on...
swentel’s picture

Status: Reviewed & tested by the community » Fixed

commited and pushed, thanks!
pushed to 8.x-1.x as well

  • swentel committed 412a3d7 on 8.x-1.x authored by charginghawk
    Issue #2951175 by charginghawk, dion-jensen, swentel: Infinite recursion...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

adam-delaney’s picture

I have recently encountered this issue with the update to Drupal 9.4.0 with a base field that does not specify weight in it's form display.

moshe weitzman’s picture

I'm seeing same as Adam Delaney. The workaround is to specify the weight.

For some reason $recursion_check is not working as a static variable should. Its always an empty array even on subsequent calls to field_group_field_info_max_weight().

sjerdo’s picture

I'm experiencing the same issue as adam-delaney and moshe weitzman in Drupal 9.4.3. Adding a weight fixes the issue for me (as a workaround).

gambry’s picture

@adam-delaney @moshe weitzman @sjerdo what version of the module do you use?
I noticed 1.x uses static keyword, while 3.x uses drupal_static.

Also if you get debug further and post more details that would be useful.

sjerdo’s picture

@gambry We are using field_group version 8.x-3.2 and Drupal Core 9.4.3

The error occurs on the ContentEntityForm of a custom content entity type (@ContentEntityType)

in function baseFieldDefinitions we changed the following field definition:

    $fields['reusable'] = BaseFieldDefinition::create('boolean')
      ->setLabel(new TranslatableMarkup('Can be used multiple times'))
      ->setDescription(new TranslatableMarkup('A boolean indicating whether the discount can be applied multiple times.'))
      ->setDefaultValue(TRUE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
      ])
      ->setDisplayConfigurable('form', FALSE);

and added a weight to 'fix' the issue:

    $fields['reusable'] = BaseFieldDefinition::create('boolean')
      ->setLabel(new TranslatableMarkup('Can be used multiple times'))
      ->setDescription(new TranslatableMarkup('A boolean indicating whether the discount can be applied multiple times.'))
      ->setDefaultValue(TRUE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
        'weight' => 10,
      ])
      ->setDisplayConfigurable('form', FALSE);
donquixote’s picture

Actually another perhaps "cleaner" solution would be if `field_group_info_groups()` would not load the complete view mode / form mode, but just the raw config data. But I don't know if that is possible or would cause other problems.

donquixote’s picture

Actually the entire mechanism and hook in core are quite problematic and seem to be not very useful.
The only implementation I could find is from field group.

And I wonder if it ever does anything useful. I would think that it runs into the infinite recursion.