Problem/Motivation

Since Drupal 11.3 core renders placeholders and lazy builders inside fibers, and entity storage suspends the current fiber in EntityStorageBase::loadMultiple() so that several placeholders can batch their loads. PHP has a bug where a fiber that suspends inside a magic method trips the engine's recursion guard for that property, so the next read of the same property on the same object skips __get() and reads a real property that does not exist (https://github.com/php/php-src/issues/14983). Core worked around it in #3565937: Workaround PHP bug with fibers and __get() for EntityReferenceFieldItemList and EntityReferenceItemBase.

The {type}_profiles fields that profile adds to the user entity use their own list class, ProfileEntityFieldItemList, which is computed. Reading $user->customer_profiles->entity goes through FieldItemList::__get(), which calls first(), which computes the list by loading the profiles, which suspends the fiber while still inside __get(). Core's workaround sits one level down on the item and is never reached. The result is this warning and a NULL where the profile should be:

Warning: Undefined property: Drupal\profile\Plugin\Field\ProfileEntityFieldItemList::$entity

You see it whenever two lazy builders or placeholders on the same page both read a profile off the same user. Core runs each of them in a fiber and pauses the first one at the profile load, so the second read on that user object hits the recursion guard and gets NULL. A single read on a page never triggers it, which makes it look intermittent.

Steps to reproduce

  1. Drupal 11.3 or later, profile 8.x-1.14, a profile type, a user with one profile of that type.
  2. From a lazy builder or any code running inside a fiber, read $user->{type}_profiles->entity, then read it again on the same user object before the fiber resumes.
  3. The second read logs the warning above and returns NULL.

The kernel test on the MR does this directly with a \Fiber, the same way core's EntityReferenceFieldTest::testEntityReferenceListFiberSuspension() does.

Proposed resolution

Add the same __get() workaround core uses in EntityReferenceItemBase to ProfileEntityFieldItemList: when a fiber is current and the property is entity, run the parent getter inside a private fiber and drive it to completion before returning. Nothing changes outside a fiber.

Issue fork profile-3621620

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

loze created an issue. See original summary.

loze’s picture

Issue summary: View changes

loze changed the visibility of the branch 3621620-fiber-guard-computed-lists to hidden.

loze’s picture

Status: Active » Needs review

!44 is up against 8.x-1.x. The new ProfileFiberTest passes in the pipeline, and phpcs, cspell, eslint and stylelint are green.

Two jobs are red and neither is from this MR. In phpunit, Profile2MigrateTest errors twice on a missing Drupal\filter\FilterFormatRepositoryInterface service when installing profile's config, and ProfileDefaultTest::testProfileFieldOnUserDisplayConfig fails to find the field label on the user display page.

I ran that second one locally on 11.4 with and without this change and it fails the same way both times, so it is the current core the pipeline pulls and nothing this branch touches.

The seven phpstan findings are all in files this MR does not change: the 11.3 CacheableMetadata parameter on the two list builders, the deprecated DrupalSqlBase parent in the Profile2 source plugin, user_load_by_name() in ProfileRegisterFormTest, the private property in ProfileLocalAction, and the stored storage property in two kernel tests. The last pipeline on 8.x-1.x itself was green in December 2025 against the core of that time, so those are core drift since then and belong in their own issues.

Some of this work was done with help from an AI coding agent. I have gone through it and verified it.