Skip to content

[#270] Fixed 'entityCreate()' base field expansion and dynamic id key resolution.#346

Merged
AlexSkrypnyk merged 3 commits intomasterfrom
feature/entity-create
Apr 19, 2026
Merged

[#270] Fixed 'entityCreate()' base field expansion and dynamic id key resolution.#346
AlexSkrypnyk merged 3 commits intomasterfrom
feature/entity-create

Conversation

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

@AlexSkrypnyk AlexSkrypnyk commented Apr 19, 2026

Iterates on #271 by @chrisolof. Thank you for the contribution!

Closes #270

Summary

Fixes two bugs in the generic entityCreate() / entityDelete() pipeline that blocked setting base entity-reference fields on entity stubs (e.g. commerce_product.variations, user.roles) and caused stub-based round-trips to fail for any entity type whose id key isn't literally id.

  1. expandEntityFields() previously only processed configured fields (FieldStorageConfig); base fields were filtered out unless the caller explicitly passed them in $base_fields. entityCreate() never did, so base entity-reference fields set on the stub reached entity storage in their raw scalar form and failed to resolve targets by label. The method now auto-detects base fields present as properties on the stub (excluding the id and bundle keys, which are system-managed) and routes them through the field-handler pipeline alongside configured fields.

  2. entityCreate() populated $entity->id and entityDelete() loaded from $entity->id, hardcoding the generic property name. For entity types whose id key isn't id (useruid, nodenid, taxonomy_termtid, plus most commerce and custom types), this broke stub-based round-trips and was inconsistent with nodeCreate/userCreate/termCreate, which already populate the proper id key. Both methods now resolve the id key via $definition->getKey('id') and use it consistently.

v3.x behaviour change: entityCreate() now populates $entity->$id_key (e.g. $stub->uid for user entities) instead of $entity->id. Consumers that read $entity->id after entityCreate() must switch to $entity->$id_key or read from the returned EntityInterface.

Changes

src/Drupal/Driver/Core/Core.php

  • expandEntityFields(): merges explicitly passed $base_fields with a new auto-detected set before calling getEntityFieldTypes().
  • New protected detectBaseFieldsOnEntity(): walks EntityFieldManager::getBaseFieldDefinitions() and returns the base field names that exist as properties on the stub, excluding the entity type's id key and bundle key.
  • entityCreate(): looks up the entity type definition once, resolves $bundle_key and $id_key from it, and populates $entity->$id_key = $created_entity->id() after save. The bundle-validation guard and step_bundle promotion are unchanged.
  • entityDelete(): when a stub is passed (rather than a loaded EntityInterface), resolves the id key the same way and loads via $entity->$id_key.

tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php

  • testEntityCreateAndDeleteWithStub: asserts $stub->uid is populated and $stub->id is NOT, and uses $stub->uid for the round-trip entityDelete().
  • New testEntityCreateAutoExpandsBaseFieldsSetOnStub: calls entityCreate('user', $stub) with a plain scalar name, then asserts $stub->name === ['uma'] — proving DefaultHandler::expand() ran on the base field.

Before / After

expandEntityFields(entity_type, entity):

Before:
  base_fields = caller-supplied only
  getEntityFieldTypes(type, base_fields)
    -> returns configured FieldStorageConfig entries
       + any base fields the caller named explicitly

  Stub has $entity->variations = ['sku-1', 'sku-2']
  commerce_product.variations is a BASE entity_reference field
  -> not in caller-supplied base_fields
  -> filtered out in getEntityFieldTypes
  -> EntityReferenceHandler NEVER called
  -> Drupal storage receives ['sku-1', 'sku-2'] (raw labels)
  -> save fails / silently drops

After:
  auto = detectBaseFieldsOnEntity(type, entity)   <-- NEW
       = base fields present on the stub, minus id/bundle keys
  base_fields = unique(caller-supplied + auto)
  getEntityFieldTypes(type, base_fields)
    -> now includes 'variations'
  -> EntityReferenceHandler resolves each label to a target id
  -> storage receives the shape it expects
entityCreate() / entityDelete() id-key resolution:

Before:                              After:
  $created->save();                    $id_key = $definition->getKey('id');
  $entity->id = $created->id();        ...
                                       $created->save();
                                       $entity->$id_key = $created->id();
  entityDelete():                      entityDelete():
    $storage->load($entity->id)          $storage->load($entity->$id_key)

  user stub:                           user stub:
    { name, mail, status }               { name, mail, status }
  after create: ->id = 42              after create: ->uid = 42
  delete: load(user, 42) via ->id      delete: load(user, 42) via ->uid
  (works only because we set ->id)     (works using the entity's real id key)

Summary by CodeRabbit

  • Bug Fixes

    • Entity create/delete now respect entity-type identifier keys (e.g., uid) instead of assuming a generic id.
    • Base fields set on stubs (including base entity-reference fields and product-variation references) are automatically expanded and routed through the field pipeline.
  • Tests

    • Added kernel tests validating stub expansion, entity-reference behavior, and commerce product/variation creation.
  • Chores

    • Added a development dependency to enable commerce-related tests.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

Warning

Rate limit exceeded

@AlexSkrypnyk has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ab41693-5d11-4f7a-923d-f696aabdf241

📥 Commits

Reviewing files that changed from the base of the PR and between b080355 and 04b20e8.

📒 Files selected for processing (2)
  • src/Drupal/Driver/Core/Core.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
📝 Walkthrough

Walkthrough

Detects base-field properties present on entity stubs and auto-expands them through the field-handler pipeline; entityCreate/entityDelete now use the entity type’s configured ID and bundle keys (e.g., uid) instead of assuming a generic id.

Changes

Cohort / File(s) Summary
Core driver logic
src/Drupal/Driver/Core/Core.php
Added detectBaseFieldsOnEntity(); expandEntityFields() now augments base fields with properties found on stubs; entityCreate() derives bundle_key and id_key from the entity type and writes the created entity identifier onto the stub using the type-specific id key; entityDelete() reads the id using the type id_key.
Core kernel tests (user behavior)
tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
Updated testEntityCreateAndDeleteWithStub() assertions to expect type-specific id key (uid) and removed assumptions about generic id; added testEntityCreateAutoExpandsBaseFieldsSetOnStub() and testEntityCreateExpandsBaseEntityReferenceFieldOnStub() to verify base-field expansion (including entity-reference base fields).
Commerce kernel test
tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityCreateCommerceKernelTest.php
Added new kernel test exercising entityCreate() for commerce_product_variation and commerce_product, asserting that base-field variations on a product stub is expanded/resolved to created variation ids and that stubs receive the correct type-specific id keys.
Dependencies
composer.json
Added drupal/commerce to require-dev for the new commerce kernel test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Issue #270: Addresses the same code-level problems—adding base-field detection/expansion on stubs and using entity type id keys instead of assuming id.

Possibly related PRs

  • PR #303: Related changes to base-field detection/resolution logic; overlaps with the new stub base-field expansion.
  • PR #338: Modifications to the Core driver and handler lookup/structure that touch the same implementation area.

Poem

🐰
With whiskers twitching, I hop through the code,
Finding base fields where stubs once strode.
No generic id hides what should be seen—
uid, product_id, each fits the scene.
Hooray for fields that now flow clean!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main changes: fixing base field expansion and dynamic id key resolution in entityCreate()/entityDelete() methods.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/entity-create

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 19, 2026

Code coverage (threshold: 95%)



Code Coverage Report Summary:
  Classes: 100.00% (22/22)
  Methods: 100.00% (152/152)
  Lines:   100.00% (725/725)

Per-class coverage
Drupal\Driver\BlackboxDriver                                 100.00%
Drupal\Driver\Core\Core                                      100.00%
Drupal\Driver\Core\Field\AbstractHandler                     100.00%
Drupal\Driver\Core\Field\AddressHandler                      100.00%
Drupal\Driver\Core\Field\DaterangeHandler                    100.00%
Drupal\Driver\Core\Field\DatetimeHandler                     100.00%
Drupal\Driver\Core\Field\DefaultHandler                      100.00%
Drupal\Driver\Core\Field\EmbridgeAssetItemHandler              0.00%
Drupal\Driver\Core\Field\EntityReferenceHandler              100.00%
Drupal\Driver\Core\Field\FileHandler                         100.00%
Drupal\Driver\Core\Field\ImageHandler                        100.00%
Drupal\Driver\Core\Field\LinkHandler                         100.00%
Drupal\Driver\Core\Field\ListFloatHandler                      0.00%
Drupal\Driver\Core\Field\ListHandlerBase                     100.00%
Drupal\Driver\Core\Field\ListIntegerHandler                    0.00%
Drupal\Driver\Core\Field\ListStringHandler                     0.00%
Drupal\Driver\Core\Field\NameHandler                         100.00%
Drupal\Driver\Core\Field\OgStandardReferenceHandler            0.00%
Drupal\Driver\Core\Field\SupportedImageHandler               100.00%
Drupal\Driver\Core\Field\TaxonomyTermReferenceHandler        100.00%
Drupal\Driver\Core\Field\TextWithSummaryHandler              100.00%
Drupal\Driver\Core\Field\TimeHandler                         100.00%
Drupal\Driver\DrupalDriver                                   100.00%
Drupal\Driver\DrushDriver                                    100.00%
Drupal\Driver\Exception\BootstrapException                   100.00%
Drupal\Driver\Exception\Exception                            100.00%
Drupal\Driver\Exception\UnsupportedDriverActionException     100.00%

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php (2)

90-99: Exercise an entity-reference base field in this regression test.

name only proves DefaultHandler runs. Since the fixed bug specifically mentions base entity-reference fields like user.roles, add a roles assertion so the test would fail if EntityReferenceHandler is skipped.

Suggested test expansion
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Driver\Core\Core;
 use Drupal\entity_test\EntityTestHelper;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
 use PHPUnit\Framework\Attributes\Group;
   public function testEntityCreateAutoExpandsBaseFieldsSetOnStub(): void {
+    $role = Role::create([
+      'id' => 'base_field_editor',
+      'label' => 'Base field editor',
+    ]);
+    $role->save();
+
     $stub = (object) [
       'name' => 'uma',
       'mail' => 'uma@example.com',
       'status' => 1,
+      'roles' => 'Base field editor',
     ];
 
-    $this->core->entityCreate('user', $stub);
+    $created = $this->core->entityCreate('user', $stub);
 
     $this->assertSame(['uma'], $stub->name, 'base field "name" was routed through the handler pipeline.');
+    $this->assertSame(['base_field_editor'], $stub->roles, 'base entity-reference field "roles" was routed through the handler pipeline.');
+    $this->assertInstanceOf(User::class, $created);
+    $this->assertTrue($created->hasRole('base_field_editor'));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php` around
lines 90 - 99, Expand the testEntityCreateAutoExpandsBaseFieldsSetOnStub test to
exercise a base entity-reference field: include a 'roles' entry on the $stub (in
addition to 'name', 'mail', 'status') before calling
$this->core->entityCreate('user', $stub), and add an assertion (e.g.
$this->assertSame([...], $stub->roles)) that the roles property was transformed
by the EntityReferenceHandler; this ensures the EntityReferenceHandler runs the
same pipeline as DefaultHandler and will fail if it is skipped.

66-76: Assert the stub ID matches the created entity.

assertNotEmpty() proves uid was set, but not that it is the ID returned by storage. A direct comparison makes this regression test tighter.

Suggested assertion
     $created = $this->core->entityCreate('user', $stub);
 
     $this->assertInstanceOf(EntityInterface::class, $created);
-    $this->assertNotEmpty($stub->uid, 'entityCreate populated the entity type id key (uid) on the stub.');
+    $this->assertSame((string) $created->id(), (string) $stub->uid, 'entityCreate populated the created entity id under the entity type id key (uid) on the stub.');
     $this->assertFalse(property_exists($stub, 'id'), 'entityCreate did not populate a generic "id" property on the stub.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php` around
lines 66 - 76, Add an assertion that the ID populated on the stub matches the ID
of the created entity: after calling $this->core->entityCreate('user', $stub)
and before deleting, assert that the created entity's id() (or the ID getter
used by the returned EntityInterface instance, referenced as $created) equals
the stub's uid property ($stub->uid) to ensure the stub was populated with the
actual storage ID.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Drupal/Driver/Core/Core.php`:
- Around line 824-827: The code dereferences $entity->$id_key without ensuring
the stub contains a valid ID; update the guard around the EntityInterface check
in Core:: (the block using $entity, $id_key and
\Drupal::entityTypeManager()->getStorage($entity_type)->load(...)) to verify
that the stub has a non-empty property named by $id_key (e.g.
isset($entity->{$id_key}) && $entity->{$id_key}) before calling load; if the ID
is missing, handle it explicitly (throw a clear exception or return null/false
per surrounding contract) instead of calling load(NULL) so pre-v3 or invalid
stubs produce a clear failure path.

---

Nitpick comments:
In `@tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php`:
- Around line 90-99: Expand the testEntityCreateAutoExpandsBaseFieldsSetOnStub
test to exercise a base entity-reference field: include a 'roles' entry on the
$stub (in addition to 'name', 'mail', 'status') before calling
$this->core->entityCreate('user', $stub), and add an assertion (e.g.
$this->assertSame([...], $stub->roles)) that the roles property was transformed
by the EntityReferenceHandler; this ensures the EntityReferenceHandler runs the
same pipeline as DefaultHandler and will fail if it is skipped.
- Around line 66-76: Add an assertion that the ID populated on the stub matches
the ID of the created entity: after calling $this->core->entityCreate('user',
$stub) and before deleting, assert that the created entity's id() (or the ID
getter used by the returned EntityInterface instance, referenced as $created)
equals the stub's uid property ($stub->uid) to ensure the stub was populated
with the actual storage ID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73c60999-c478-439a-8743-5c11e5e470be

📥 Commits

Reviewing files that changed from the base of the PR and between 34050e2 and ae21d73.

📒 Files selected for processing (2)
  • src/Drupal/Driver/Core/Core.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php

Comment thread src/Drupal/Driver/Core/Core.php
… via 'user.roles' and 'commerce_product.variations'.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityCreateCommerceKernelTest.php (1)

123-126: Reset entity storage cache before asserting the persisted relationship.

The test is meant to verify the saved product state. Clearing the product storage cache before loading avoids accidentally asserting against a cached entity instance.

Proposed test-strengthening change
-    $product = Product::load((int) $product_stub->product_id);
+    $product_storage = $this->container->get('entity_type.manager')->getStorage('commerce_product');
+    $product_storage->resetCache([(int) $product_stub->product_id]);
+    $product = $product_storage->load((int) $product_stub->product_id);
     $this->assertInstanceOf(Product::class, $product);
 
     $variation_ids = array_map(intval(...), $product->getVariationIds());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityCreateCommerceKernelTest.php`
around lines 123 - 126, The test should clear the product entity storage cache
before reloading to ensure you assert against persisted data rather than a
cached instance; in CoreEntityCreateCommerceKernelTest, obtain the
commerce_product storage via the entity type manager (e.g.
getStorage('commerce_product')) and call resetCache() on it before calling
Product::load((int) $product_stub->product_id), then proceed with
$this->assertInstanceOf and the variation id extraction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityCreateCommerceKernelTest.php`:
- Around line 123-126: The test should clear the product entity storage cache
before reloading to ensure you assert against persisted data rather than a
cached instance; in CoreEntityCreateCommerceKernelTest, obtain the
commerce_product storage via the entity type manager (e.g.
getStorage('commerce_product')) and call resetCache() on it before calling
Product::load((int) $product_stub->product_id), then proceed with
$this->assertInstanceOf and the variation id extraction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d05f9c8-4f91-4293-b91f-1a045a4edde2

📥 Commits

Reviewing files that changed from the base of the PR and between ae21d73 and b080355.

📒 Files selected for processing (3)
  • composer.json
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityCreateCommerceKernelTest.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
✅ Files skipped from review due to trivial changes (1)
  • composer.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php

@AlexSkrypnyk AlexSkrypnyk merged commit e49d091 into master Apr 19, 2026
12 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/entity-create branch April 19, 2026 23:13
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.

Entity creation does not expand base fields & assumes "id" for entity ID key (D8/9/10 driver)

1 participant