Skip to content

Bundle fields not accounted if they are created through hook_entity_bundle_field_info()#356

Open
idimopoulos wants to merge 2 commits intojhedstrom:2.xfrom
idimopoulos:bundle_computed
Open

Bundle fields not accounted if they are created through hook_entity_bundle_field_info()#356
idimopoulos wants to merge 2 commits intojhedstrom:2.xfrom
idimopoulos:bundle_computed

Conversation

@idimopoulos
Copy link
Copy Markdown
Contributor

So the Drupal8 driver seems to have been expanded in 71a97a3 as far as I can track down, but it seems to be missing the bundle fields that are created through hook_entity_bundle_field_info and are computed.

Disclaimer
I am not really good with Unit tests and I have to sum up my day to go to the DDD the upcoming days so the test was LLM assisted. Feel free to disregard.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac9a53c7-e0a9-4f7d-84f9-44af6973023d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

Thanks for the PR. Before we land a fix, let me walk through how this driver currently decides which fields get routed through its handler pipeline, so the right shape of a fix falls out naturally.

The two axes of a field

Every Drupal field carries two independent attributes that matter here:

Origin (where the definition is declared):

  1. EntityClass::baseFieldDefinitions() - entity-type-wide base fields on the entity class.
  2. FieldStorageConfig + FieldConfig - configurable fields, typically created through the UI or config sync.
  3. EntityClass::bundleFieldDefinitions() or hook_entity_bundle_field_info() - bundle-specific field definitions. Per-bundle only.
  4. hook_entity_field_storage_info() - entity-type-wide storage declaration, usually paired with a bundle-specific definition from path 3.

Storage profile (how the value is persisted):

  • Standard storage - Drupal manages the schema. Value round-trips through entity storage normally.
  • Computed (setComputed(TRUE)) - no storage. The value is derived at read time by the FieldItemListInterface class. Assigning a value on a stub is a no-op; Drupal discards it on save.
  • Custom storage (setCustomStorage(TRUE)) - the declaring module owns persistence. The normal entity storage layer does not write the value out.

What EntityFieldManager surfaces per combination

Origin Storage In getFieldStorageDefinitions() In getBaseFieldDefinitions() In getFieldDefinitions($bundle)
baseFieldDefinitions() standard yes yes yes
baseFieldDefinitions() computed no (filtered at EntityFieldManager::getFieldStorageDefinitions() line 472) yes yes
baseFieldDefinitions() custom yes yes yes
FieldStorageConfig + FieldConfig standard yes no yes
bundleFieldDefinitions() alone computed no no yes
bundleFieldDefinitions() alone custom no no yes
bundleFieldDefinitions() alone standard (not supported by default content-entity schema) no no yes
hook_entity_field_storage_info() + bundleFieldDefinitions() standard yes no yes

The "bundle-only with standard storage" row exists in the entity-field tables but has nowhere for Drupal's SqlContentEntityStorage to read from or write to, because the default schema generator only considers entity-type-wide definitions. In practice, bundle-only fields declare either computed, custom storage, or come paired with hook_entity_field_storage_info().

How the driver consumes those lookups

getEntityFieldTypes() iterates getFieldStorageDefinitions() + (optionally) getBaseFieldDefinitions(). A field is kept for handler expansion only if either predicate returns TRUE:

  • isField() / fieldExists() - TRUE only if the field is a FieldStorageConfig instance. Covers configurable fields.
  • isBaseField() / fieldIsBase() - TRUE only if the field is in getBaseFieldDefinitions(), AND the caller passed the field name in the $base_fields argument. Covers entity-type-wide base fields.

Applying that to the matrix above:

  • Standard-storage entity-type-wide base fields: picked up. Handler expansion works.
  • Computed entity-type-wide base fields: classified as base, but setting a value is meaningless (Drupal derives, not stores).
  • Custom-storage entity-type-wide base fields: classified as base, handler expands the value, but the module's custom storage layer may or may not consume the expanded form.
  • Configurable fields: picked up. Handler expansion works.
  • Bundle-only fields, any storage profile: not picked up. Absent from both predicates' source lookups.
  • Bundle-declared fields paired with hook_entity_field_storage_info(): the storage lands in getFieldStorageDefinitions(), so the loop iterates the field, but both predicates return FALSE (not a FieldStorageConfig instance, not in getBaseFieldDefinitions()). The field is iterated and discarded.

The real gap

The one case worth caring about is the last one: a non-computed, standard-storage field declared at the entity-type level through hook_entity_field_storage_info() and attached per-bundle through hook_entity_bundle_field_info(). It has persistable storage, scenario authors can reasonably want to set values for it, and the driver currently does not route it through handler expansion.

Closing that properly needs coordinated changes at both the predicate layer and the iteration-source layer, with test coverage for the round-trip. That is something I am happy to take on as a maintainer, so there is no need to rework this PR on your side.

What would help finalize the fix

Could you share a concrete example of what you are running into? Specifically:

  1. The actual field declarations: the hook_entity_bundle_field_info() implementation (or bundleFieldDefinitions() method), and whether a hook_entity_field_storage_info() sibling exists for the same field.
  2. Whether the field is computed and whether it uses custom storage.
  3. A step definition, scenario, or PHPUnit test that fails today without this change - ideally showing the value being set via a stub and what you expect to read back.

That gives us a reproducible starting point and confirms which of the paths above is actually in play for your setup.

@idimopoulos
Copy link
Copy Markdown
Contributor Author

Apologies for the delay @AlexSkrypnyk .. As we have (tens of) thousands of steps in behat in our project, these latest updates caused a crazy amount of failures and I wanted to first fix them all and verify our project before coming back to this as I was not sure if it was an issue.
2 of our cases:

  • meta_entity module. This module is providing entities that are supposed to be side-entities of other entities (like settings) and create automatically reverse references as computed budnle fields. Now, this one, we are handling with a before node create and after node create hooks to consume and set the values (as the parent entity needs to be saved first).
  • rdf_sync module. Creates a field with custom storage that stores the URI of the exportable entities in a separate table as a bundle field via a hook. Unlike the above case though, this can be used to designate the URI of the entity during the entity creation (so that assertions can be properly made).

Now, we have actually many more cases but I don't want to put them all here, it will get long and boring, but I can still add them if you want ofc.
The issue comes from the fact that the RawDrupalContext::parseEntityFields() now rejects fields that are not recognized (previously silently ignoring them).
To be fair, I don't know if the best solution here is to recognize them or silently reject them, but I would go with the first, as the rejection, even though it gave me a lot of headache in our tests, it did reveal a lot of leftover values that were cleaned/updated now.

I would say that recognizing these computed fields mostly, would not really be a problem because the user that uses them, would bear the burden of handling them in a before/after node create hook.

For reference, here are the two code blocks from the above modules that create the fields:

# Meta entity
function meta_entity_entity_bundle_field_info(EntityTypeInterface $entity_type, ?string $bundle, array $base_field_definitions): array {
  /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
  $bundle_info = \Drupal::service('entity_type.bundle.info');
  $entity_type_id = $entity_type->id();
  $bundle = $bundle ?? '';

  $fields = [];
  foreach (\Drupal::getContainer()->getParameter('meta_entity.repositories') as $meta_entity_type_id => $service_id) {
    /** @var \Drupal\meta_entity\MetaEntityRepositoryInterface $repository */
    $repository = \Drupal::service($service_id);
    foreach ($repository->getReverseReferenceFieldNames($entity_type_id, $bundle) as $meta_entity_bundle => $field_name) {
      /** @var \Drupal\Core\Field\BaseFieldDefinition $definition */
      $bundle_label = $bundle_info->getBundleInfo($meta_entity_type_id)[$meta_entity_bundle]['label'];
      $fields[$field_name] = BaseFieldDefinition::create('entity_reference')
        ->setName($field_name)
        ->setTargetEntityTypeId($entity_type_id)
        ->setTargetBundle($bundle)
        ->setLabel(t('@label reference', ['@label' => $bundle_label]))
        ->setDescription(t('@label attached metadata.'))
        ->setSetting('target_type', $meta_entity_type_id)
        ->setSetting('meta_entity_type_id', $meta_entity_bundle)
        ->setCardinality(1)
        ->setDisplayConfigurable('view', TRUE)
        ->setComputed(TRUE)
        ->setClass(MetaEntityReverseReferenceItemList::class);
    }
  }
  return $fields;
}
# RDF Sync
function rdf_sync_entity_bundle_field_info(EntityTypeInterface $entityType, ?string $bundle, array $baseFieldDefinitions): array {
  $fields = [];
  $mapper = \Drupal::getContainer()->get('rdf_sync.mapper');
  if ($bundle && $mapper->isMappedBundle($entityType->id(), $bundle)) {
    $fieldName = $mapper->getRdfUriFieldName($entityType->id(), $bundle);
    $fields[$fieldName] = BundleFieldDefinition::create('uri')
      ->setName($fieldName)
      ->setTargetEntityTypeId($entityType->id())
      ->setTargetBundle($bundle)
      ->setLabel(t('URI'))
      ->setRequired(TRUE)
      ->setComputed(TRUE)
      ->setCustomStorage(TRUE)
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE)
      ->setRevisionable(FALSE)
      ->setClass(RdfSyncUriFieldItemList::class);
  }
  return $fields;
}

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

AlexSkrypnyk commented Apr 29, 2026

@idimopoulos
Thank you for providing code snippets

  1. The better field handling will be addressed in 6.x of Drupal Extension because DrupalDriver resolves it in about-to-be-released version 3.x. There are a lot of complex cases and missing testing infra on 5.x of Drupal Extension project and 2.x of DrupalDriver so the path forward was only possible with a full refactor.
  2. To unblock you, I suggest you extend Drupal Context in your own custom context and override parseEntityFields() to remove that field validation added in Drupal Extension 5.3.
  3. Please do not work on this PR. I will iterate on it myself for 3.x. The reason for this is that there is no way to test your PR in 2.x even if tests passing locally) as your changes may break other projects. 3.x has a proper test harness that addresses this issue. You can see the number of different types of fields in https://github.com/jhedstrom/DrupalDriver/blob/master/src/Drupal/Driver/Core/Field/README.md

Thank you for your patience.

@idimopoulos
Copy link
Copy Markdown
Contributor Author

Thank you for your prompt response @AlexSkrypnyk . I will not work anymore on this then. In any case, the patch worked for us and I have downloaded it locally. For further updates, I will wait to see how 6.x will be :) Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants