Skip to content

[#365] Added creation hint registry for entity stub aliases.#369

Open
AlexSkrypnyk wants to merge 11 commits into
masterfrom
feature/365-creation-hints
Open

[#365] Added creation hint registry for entity stub aliases.#369
AlexSkrypnyk wants to merge 11 commits into
masterfrom
feature/365-creation-hints

Conversation

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

@AlexSkrypnyk AlexSkrypnyk commented May 12, 2026

Closes #365

Summary

Prior to this change, ergonomic stub properties such as author on nodes, parent on terms, and roles on users were resolved by scattered inline if ($stub->hasValue(...)) blocks inside Core.php and DrushDriver.php with no discoverable contract. This PR introduces a first-class creation hint system: a CreationHintCapabilityInterface that exposes a typed registry per entity type, two lifecycle interfaces (PreCreateHintInterface and PostCreateHintInterface), and a CreationHintRegistryTrait that both drivers now use to run pre- and post-save dispatch. As a 3.0 behavioural break, unresolvable hints (non-existent author, empty author, missing vocabulary on a parent term) now throw CreationHintResolutionException instead of silently producing wrong values.

Changes

Capability interface

  • Added Drupal\Driver\Capability\CreationHintCapabilityInterface with getCreationHints(string $entity_type): array. Composite driver interfaces (DrupalDriverInterface, DrushDriverInterface) are intentionally not updated - hand-rolled implementations remain valid. BlackboxDriver deliberately does not implement the capability.

Hint interfaces and exception

  • Added Drupal\Driver\Hint\CreationHintInterface (base contract: getAlias(), getDescription()).
  • Added Drupal\Driver\Hint\PreCreateHintInterface - mutates the stub before EntityInterface::save().
  • Added Drupal\Driver\Hint\PostCreateHintInterface - acts on the entity after save.
  • Added Drupal\Driver\Exception\CreationHintResolutionException extending InvalidArgumentException.

Hint implementations

  • Core\Hint\AuthorHint - resolves author stub alias to a Drupal user UID (pre-create).
  • Core\Hint\VocabularyMachineNameHint - resolves vocabulary alias to vid on term stubs (pre-create).
  • Core\Hint\ParentTermHint - resolves parent term name to a TID (pre-create).
  • Hint\RolesHint - assigns roles array after user save (post-create), shared by Core and DrushDriver.

Registry trait and dispatcher wiring

  • Added Drupal\Driver\Hint\CreationHintRegistryTrait with getCreationHints(), runPreCreateHints(), and runPostCreateHints() helpers.
  • Core, DrupalDriver, and DrushDriver now use the trait. Inline alias blocks in Core::nodeCreate(), Core::termCreate(), Core::userCreate(), DrushDriver::termCreate(), and DrushDriver::userCreate() replaced with dispatcher calls.
  • Core::userCreate() now honours roles on the stub (symmetry fix - DrushDriver already did).

Tests

  • 27 hint unit tests across AuthorHintTest, VocabularyMachineNameHintTest, ParentTermHintTest, and RolesHintTest.
  • 3 driver-level registry test classes: DrupalDriverCreationHintsTest, DrushDriverCreationHintsTest, BlackboxDriverCreationHintsTest.
  • 3 new kernel tests: vocabulary machine-name positive, author-not-found error, Core roles round-trip.

Documentation

  • UPGRADING.md section for the 3.0 behavioural break: CreationHintResolutionException on bad author, empty author, and missing vocabulary when using parent.

Error message improvements

  • VocabularyMachineNameHint::applyToStub() guarded against a missing alias.
  • ParentTermHint throws with a clear message when the vocabulary cannot be resolved.
  • Core::nodeCreate() throws a clear error on empty author.

Before / After

BEFORE - scattered inline blocks, no discoverable contract
==========================================================

Core.php / DrushDriver.php
  nodeCreate($stub)                termCreate($stub)             userCreate($stub)
  |                                |                             |
  if ($stub->hasValue('author'))   if ($stub->hasValue('vocab')) if ($stub->hasValue('roles'))
    $uid = user lookup               $stub->vid = lookup           (only in Drush)
    $stub->uid = $uid              if ($stub->hasValue('parent'))
    // silently wrong on miss        $stub->parent = TID lookup

  No registry.  No contract.  Missing vocab -> silent wrong value.
  Core::userCreate() ignores 'roles' entirely.


AFTER - registry + lifecycle dispatch
======================================

CreationHintCapabilityInterface
  getCreationHints(entity_type): array<alias => CreationHintInterface>

CreationHintRegistryTrait
  runPreCreateHints($stub, $entity_type)   <-- called before entity save
  runPostCreateHints($entity, $entity_type) <-- called after entity save

Hint classes (each owns its alias, description, and resolution logic)
  AuthorHint          -> PreCreateHintInterface   (node: author -> uid)
  VocabularyMachineNameHint -> PreCreateHintInterface (term: vocabulary -> vid)
  ParentTermHint      -> PreCreateHintInterface   (term: parent -> tid)
  RolesHint           -> PostCreateHintInterface  (user: roles -> addRole())

Driver / Core wiring
  Core::nodeCreate()   -> runPreCreateHints() -> save()
  Core::termCreate()   -> runPreCreateHints() -> save()
  Core::userCreate()   -> runPreCreateHints() -> save() -> runPostCreateHints()
  DrushDriver same pattern.

  Unresolvable hint -> CreationHintResolutionException (not silent wrong value).
  Core::userCreate() now honours 'roles' (parity with Drush).

Summary by CodeRabbit

  • New Features

    • Creation-hints system for entity creation: author, parent-term, vocabulary alias, and post-create role assignment; hints run pre- or post-persistence.
  • Bug Fixes

    • Unresolvable hints now surface as explicit exceptions (e.g., unknown author); vocabulary alias handling is applied during creation to pick the correct vocabulary.
  • Documentation

    • UPGRADING notes added describing the v2→v3 migration and hint behavior changes.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bad0c8f4-3e1f-464a-82a6-76f6cb98639b

📥 Commits

Reviewing files that changed from the base of the PR and between 0405960 and 56da794.

📒 Files selected for processing (3)
  • src/Drupal/Driver/Core/Hint/AuthorHint.php
  • src/Drupal/Driver/Core/Hint/ParentTermHint.php
  • src/Drupal/Driver/DrupalDriver.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Drupal/Driver/Core/Hint/AuthorHint.php
  • src/Drupal/Driver/Core/Hint/ParentTermHint.php

📝 Walkthrough

Walkthrough

This PR implements creation hints as first-class, discoverable artifacts. Drivers expose named hints for entity types; hints run in pre-create or post-create pipelines, mutate stubs or apply side-effects, and unresolved hints throw CreationHintResolutionException. Core, DrupalDriver, and DrushDriver are wired to register and dispatch these hints; tests and UPGRADING.md were updated.

Changes

Creation Hints First-Class System

Layer / File(s) Summary
Creation Hint Contracts & Exception
src/Drupal/Driver/Hint/CreationHintInterface.php, src/Drupal/Driver/Hint/PreCreateHintInterface.php, src/Drupal/Driver/Hint/PostCreateHintInterface.php, src/Drupal/Driver/Capability/CreationHintCapabilityInterface.php, src/Drupal/Driver/Exception/CreationHintResolutionException.php
New interfaces define hint metadata and lifecycle (pre/post create), a capability to expose hints, and a dedicated exception for unresolved hint resolution.
Hint Registry & Dispatcher Infrastructure
src/Drupal/Driver/Hint/CreationHintRegistryTrait.php
Trait implements an entity-type-indexed registry, registration and lookup methods, and protected pre-create/post-create dispatchers that invoke applicable hints only when the stub contains the hint alias.
Core Hint Implementations & Unit Tests
src/Drupal/Driver/Core/Hint/AuthorHint.php, src/Drupal/Driver/Core/Hint/VocabularyMachineNameHint.php, src/Drupal/Driver/Core/Hint/ParentTermHint.php, src/Drupal/Driver/Hint/RolesHint.php, tests/Drupal/Tests/Driver/Unit/Core/Hint/*, tests/Drupal/Tests/Driver/Unit/Hint/*
Adds AuthorHint (resolves authoruid), VocabularyMachineNameHint (maps vocabulary_machine_namevid), ParentTermHint (resolves parent name → tid), and RolesHint (assigns roles post-create), with unit tests covering metadata, stub mutation, coercion, and error handling.
Core Driver Hint Integration & Kernel Tests
src/Drupal/Driver/Core/Core.php, tests/Drupal/Tests/Driver/Kernel/Core/*
Core implements CreationHintCapabilityInterface, registers default hints in the constructor, and calls applyPreCreateHints() before field expansion and applyPostCreateHints() after saving in nodeCreate(), userCreate(), and termCreate(); kernel tests verify author validation, vocabulary resolution, and role assignment end-to-end.
DrupalDriver & DrushDriver Hint Capability
src/Drupal/Driver/DrupalDriver.php, src/Drupal/Driver/DrushDriver.php, tests/Drupal/Tests/Driver/Unit/DrupalDriverCreationHintsTest.php, tests/Drupal/Tests/Driver/Unit/DrushDriverCreationHintsTest.php
Both drivers implement CreationHintCapabilityInterface; DrupalDriver delegates getCreationHints() to its core when supported, DrushDriver registers RolesHint directly and uses the registry pipeline for role assignment; unit tests cover capability detection, delegation, hint discovery, and replacement semantics.
Test Fixtures, Excluded Drivers & Documentation
tests/Drupal/Tests/Driver/Unit/Fixtures/*, tests/Drupal/Tests/Driver/Unit/BlackboxDriverCreationHintsTest.php, UPGRADING.md
Adds test fixtures (FakeUser, RecordingUserCapability, HintCapableCoreInterface), verifies BlackboxDriver does not implement the capability, and documents the v2→v3 migration and new CreationHintResolutionException behavior in UPGRADING.md.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I nibble hints from code and cheer,
Author, vocab, parent hop near,
Roles roll on after save and play,
Registry hums to guide the way,
Tests applaud—UPGRADING signs appear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: introducing a creation hint registry system for managing entity stub aliases centrally.
Linked Issues check ✅ Passed All coding objectives from #365 are met: creation hints are now first-class, discoverable, and implement the proposed registry pattern with proper exception handling.
Out of Scope Changes check ✅ Passed All changes are scoped to the creation hints feature. No unrelated modifications to other systems are present.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/365-creation-hints

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 May 12, 2026

Code coverage (threshold: 95%)



Code Coverage Report Summary:
  Classes: 75.00% (24/32)
  Methods: 92.04% (208/226)
  Lines:   97.30% (1046/1075)

Per-class coverage
Drupal\Driver\BlackboxDriver                                 100.00%
Drupal\Driver\Core\Core                                       96.06%
Drupal\Driver\Core\Field\AbstractHandler                      93.33%
Drupal\Driver\Core\Field\AddressHandler                      100.00%
Drupal\Driver\Core\Field\BooleanHandler                      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\EntityReferenceRevisionsHandler      91.11%
Drupal\Driver\Core\Field\FieldClassifier                      94.44%
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\TextHandler                         100.00%
Drupal\Driver\Core\Field\TextLongHandler                     100.00%
Drupal\Driver\Core\Field\TextWithSummaryHandler              100.00%
Drupal\Driver\Core\Field\TimeHandler                         100.00%
Drupal\Driver\Core\Hint\AuthorHint                            94.12%
Drupal\Driver\Core\Hint\ParentTermHint                        87.50%
Drupal\Driver\Core\Hint\VocabularyMachineNameHint             88.89%
Drupal\Driver\DrupalDriver                                    98.86%
Drupal\Driver\DrushDriver                                    100.00%
Drupal\Driver\Entity\EntityStub                              100.00%
Drupal\Driver\Exception\BootstrapException                   100.00%
Drupal\Driver\Exception\CreationHintResolutionException        0.00%
Drupal\Driver\Exception\Exception                            100.00%
Drupal\Driver\Exception\UnsupportedDriverActionException     100.00%
Drupal\Driver\Hint\CreationHintRegistryTrait                 100.00%
Drupal\Driver\Hint\RolesHint                                 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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Drupal/Driver/DrushDriver.php (1)

265-275: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when UID parsing fails before post-create hint dispatch

At Line 266, $uid can be NULL, but Lines 272–275 still dispatch post-create hints with a null-backed entity object. That can produce partial side effects and brittle behavior for hints that require a persisted identity.

Suggested fix
$result = $this->drush('user-create', $arguments, $options);
$uid = $this->parseUserId($result);

-if ($uid) {
-  $stub->setValue('uid', $uid);
-}
+if ($uid === NULL) {
+  throw new \RuntimeException('Unable to parse user id from drush user-create output.');
+}
+$stub->setValue('uid', $uid);

$account = new \stdClass();
$account->uid = $uid;

$this->applyPostCreateHints($stub, $account, 'user');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Drupal/Driver/DrushDriver.php` around lines 265 - 275, The parseUserId()
call can return NULL and you must fail fast before creating a null-backed
account and invoking post-create hooks; update the logic in DrushDriver (around
where $result = $this->drush('user-create', ...) is called) to check the $uid
returned by $this->parseUserId($result) and if it is null/false, abort the flow
(throw an exception or return an error) instead of proceeding to set
$stub->setValue('uid', ...), creating $account and calling
$this->applyPostCreateHints($stub, $account, 'user'); ensure the guard
references parseUserId, $stub and applyPostCreateHints so only a valid persisted
uid triggers the post-create hint dispatch.
src/Drupal/Driver/Core/Core.php (1)

761-765: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update missing-vocabulary error text to match new resolution paths

termCreate() now accepts vocabulary via bundle or vid (possibly filled by hints), but the exception only mentions vocabulary_machine_name, which can mislead callers.

Suggested fix
-      throw new \InvalidArgumentException("Cannot create term because it is missing the required property 'vocabulary_machine_name'.");
+      throw new \InvalidArgumentException("Cannot create term because vocabulary is missing. Provide a bundle, 'vid', or 'vocabulary_machine_name'.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Drupal/Driver/Core/Core.php` around lines 761 - 765, The exception in
termCreate() checks $vocabulary (resolved from $stub->getBundle() or
$stub->getValue('vid')) but the message only mentions 'vocabulary_machine_name';
update that InvalidArgumentException text to reflect the new resolution paths by
referencing the accepted inputs (bundle or vid) — e.g. say the term is missing a
vocabulary (bundle or vid / vocabulary_machine_name) or that neither bundle nor
vid was provided (and vid may be supplied via hints) so callers aren't misled;
modify the throw in the block where $vocabulary is empty to use the new message.
🧹 Nitpick comments (3)
src/Drupal/Driver/Hint/RolesHint.php (1)

64-66: ⚡ Quick win

Validate each role value before casting

At Line 65, unconditional (string) $role can turn invalid values into misleading role names (e.g., "Array"). Adding lightweight validation here will keep failures explicit and easier to debug.

Suggested refactor
foreach ($roles as $role) {
-  $this->driver->userAddRole($stub, (string) $role);
+  if (!is_scalar($role) || trim((string) $role) === '') {
+    continue;
+  }
+  $this->driver->userAddRole($stub, trim((string) $role));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Drupal/Driver/Hint/RolesHint.php` around lines 64 - 66, The loop in
RolesHint.php unconditionally casts each $role to string before calling
$this->driver->userAddRole($stub, (string) $role), which can hide invalid types
(e.g., arrays become "Array"); update the loop to validate each $role
first—check is_string($role) or is_scalar($role) and that the trimmed string is
non-empty, log or throw a clear error for invalid values, and only then call
$this->driver->userAddRole($stub, (string) $role); reference the foreach over
$roles and the call to userAddRole to locate where to add the validation and
error handling.
tests/Drupal/Tests/Driver/Unit/Core/Hint/ParentTermHintTest.php (1)

67-120: ⚡ Quick win

Add explicit test coverage for missing vocabulary resolution path

Please add a case where bundle is NULL and vid is absent/empty, asserting CreationHintResolutionException. That branch is part of the new strict failure behavior and is worth pinning directly in this test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/ParentTermHintTest.php` around lines
67 - 120, Add a new test case that exercises the missing-vocabulary branch:
instantiate ParentTermHint and an EntityStub with bundle set to NULL and no
'vid' key (or an empty 'vid'), call ParentTermHint::applyToStub($stub) and
assert it throws CreationHintResolutionException, also assert the original
'parent' value remains unchanged on the stub (use the same pattern as
testApplyToStubThrowsOnUnknownParent to catch the exception and assert message
contains identifying info and that EntityStub::getValue('parent') still returns
the original value).
tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php (1)

100-103: ⚡ Quick win

Strengthen coercion test with non-string inputs

dataProviderApplyToStubCoercesValueToString() currently uses string-only cases. Add at least one non-string value (e.g., 7) to verify actual coercion behavior end-to-end.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php` around lines 100
- 103, The data provider dataProviderApplyToStubCoercesValueToString() only
supplies string inputs; add a non-string case to confirm coercion occurs
end-to-end by yielding an integer input and the expected string output (for
example add a case like 'integer input' => [7, '7']) so the test exercises
actual type coercion when AuthorHint::applyToStub is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Drupal/Driver/Core/Hint/AuthorHint.php`:
- Around line 75-82: The lookup result from ($this->userLookup) stored in $user
may be an object without an id() method; before calling $user->id() in
AuthorHint.php (around the AuthorHint class handling), validate that $user is an
object exposing id() (e.g. method_exists($user, 'id') or instanceof the expected
user class) and if it does not, throw a CreationHintResolutionException with a
clear message similar to the existing "user ... does not exist" error; only call
$user->id() and proceed to $stub->setValue('uid', ...) and
$stub->removeValue('author') when the guard passes.

In `@src/Drupal/Driver/Core/Hint/ParentTermHint.php`:
- Around line 43-51: The parent lookup closure in ParentTermHint currently
returns reset($tids) and silently accepts ambiguous matches; modify the
$this->parentLookup closure so after executing the taxonomy_term entity query it
checks if count($tids) > 1 and, if so, throws a CreationHintResolutionException
with a clear ambiguity message including $parent_name and $vid; otherwise
proceed to return NULL if empty or the single tid (reset($tids)). Ensure you
reference the ParentTermHint::$parentLookup closure and throw
CreationHintResolutionException when multiple matches are detected.
- Around line 79-91: Validate that the values returned from
$stub->getValue('parent') and $stub->getBundle()/ $stub->getValue('vid') are
scalars before casting to string: retrieve them into variables (e.g. $parent_raw
and $vid_raw), check is_scalar($parent_raw) and is_scalar($vid_raw) (or null
allowed for bundle), and if not throw a CreationHintResolutionException with a
clear message referencing the offending field; only then cast to string to
produce $parent_name and $vid and call $this->parentLookup((string)$parent_name,
(string)$vid).

In `@src/Drupal/Driver/DrupalDriver.php`:
- Around line 152-159: getCreationHints currently calls getCore() which can fail
if $this->core is not initialized; update getCreationHints to safely handle an
uninitialized core by catching failures from getCore (or checking $this->core
first) and returning an empty array when core is missing, then only call
$core->getCreationHints($entity_type) if $core is an instance of
CreationHintCapabilityInterface; reference getCreationHints(), getCore(),
CreationHintCapabilityInterface and $this->core when applying the guard.

In `@UPGRADING.md`:
- Around line 413-415: The migration guide currently references the wrong
dispatcher method names; update the text to use the actual method names
runPreCreateHints() and runPostCreateHints() instead of applyPreCreateHints() /
applyPostCreateHints() so the docs match the implementation (also check nearby
mentions of registerCreationHint() and getCreationHints() to ensure
consistency).

---

Outside diff comments:
In `@src/Drupal/Driver/Core/Core.php`:
- Around line 761-765: The exception in termCreate() checks $vocabulary
(resolved from $stub->getBundle() or $stub->getValue('vid')) but the message
only mentions 'vocabulary_machine_name'; update that InvalidArgumentException
text to reflect the new resolution paths by referencing the accepted inputs
(bundle or vid) — e.g. say the term is missing a vocabulary (bundle or vid /
vocabulary_machine_name) or that neither bundle nor vid was provided (and vid
may be supplied via hints) so callers aren't misled; modify the throw in the
block where $vocabulary is empty to use the new message.

In `@src/Drupal/Driver/DrushDriver.php`:
- Around line 265-275: The parseUserId() call can return NULL and you must fail
fast before creating a null-backed account and invoking post-create hooks;
update the logic in DrushDriver (around where $result =
$this->drush('user-create', ...) is called) to check the $uid returned by
$this->parseUserId($result) and if it is null/false, abort the flow (throw an
exception or return an error) instead of proceeding to set
$stub->setValue('uid', ...), creating $account and calling
$this->applyPostCreateHints($stub, $account, 'user'); ensure the guard
references parseUserId, $stub and applyPostCreateHints so only a valid persisted
uid triggers the post-create hint dispatch.

---

Nitpick comments:
In `@src/Drupal/Driver/Hint/RolesHint.php`:
- Around line 64-66: The loop in RolesHint.php unconditionally casts each $role
to string before calling $this->driver->userAddRole($stub, (string) $role),
which can hide invalid types (e.g., arrays become "Array"); update the loop to
validate each $role first—check is_string($role) or is_scalar($role) and that
the trimmed string is non-empty, log or throw a clear error for invalid values,
and only then call $this->driver->userAddRole($stub, (string) $role); reference
the foreach over $roles and the call to userAddRole to locate where to add the
validation and error handling.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php`:
- Around line 100-103: The data provider
dataProviderApplyToStubCoercesValueToString() only supplies string inputs; add a
non-string case to confirm coercion occurs end-to-end by yielding an integer
input and the expected string output (for example add a case like 'integer
input' => [7, '7']) so the test exercises actual type coercion when
AuthorHint::applyToStub is used.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/ParentTermHintTest.php`:
- Around line 67-120: Add a new test case that exercises the missing-vocabulary
branch: instantiate ParentTermHint and an EntityStub with bundle set to NULL and
no 'vid' key (or an empty 'vid'), call ParentTermHint::applyToStub($stub) and
assert it throws CreationHintResolutionException, also assert the original
'parent' value remains unchanged on the stub (use the same pattern as
testApplyToStubThrowsOnUnknownParent to catch the exception and assert message
contains identifying info and that EntityStub::getValue('parent') still returns
the original value).
🪄 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: 88767511-3875-4ade-b120-48e1fb78d691

📥 Commits

Reviewing files that changed from the base of the PR and between 49f9ae4 and b76a8dc.

📒 Files selected for processing (27)
  • UPGRADING.md
  • src/Drupal/Driver/Capability/CreationHintCapabilityInterface.php
  • src/Drupal/Driver/Core/Core.php
  • src/Drupal/Driver/Core/Hint/AuthorHint.php
  • src/Drupal/Driver/Core/Hint/ParentTermHint.php
  • src/Drupal/Driver/Core/Hint/VocabularyMachineNameHint.php
  • src/Drupal/Driver/DrupalDriver.php
  • src/Drupal/Driver/DrushDriver.php
  • src/Drupal/Driver/Exception/CreationHintResolutionException.php
  • src/Drupal/Driver/Hint/CreationHintInterface.php
  • src/Drupal/Driver/Hint/CreationHintRegistryTrait.php
  • src/Drupal/Driver/Hint/PostCreateHintInterface.php
  • src/Drupal/Driver/Hint/PreCreateHintInterface.php
  • src/Drupal/Driver/Hint/RolesHint.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreNodeMethodsKernelTest.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreTermMethodsKernelTest.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreUserMethodsKernelTest.php
  • tests/Drupal/Tests/Driver/Unit/BlackboxDriverCreationHintsTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/ParentTermHintTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/VocabularyMachineNameHintTest.php
  • tests/Drupal/Tests/Driver/Unit/DrupalDriverCreationHintsTest.php
  • tests/Drupal/Tests/Driver/Unit/DrushDriverCreationHintsTest.php
  • tests/Drupal/Tests/Driver/Unit/Fixtures/FakeUser.php
  • tests/Drupal/Tests/Driver/Unit/Fixtures/HintCapableCoreInterface.php
  • tests/Drupal/Tests/Driver/Unit/Fixtures/RecordingUserCapability.php
  • tests/Drupal/Tests/Driver/Unit/Hint/RolesHintTest.php

Comment thread src/Drupal/Driver/Core/Hint/AuthorHint.php
Comment thread src/Drupal/Driver/Core/Hint/ParentTermHint.php
Comment thread src/Drupal/Driver/Core/Hint/ParentTermHint.php Outdated
Comment thread src/Drupal/Driver/DrupalDriver.php
Comment thread UPGRADING.md
Comment on lines +413 to +415
`registerCreationHint()`, `getCreationHints()`, and two protected
dispatchers (`applyPreCreateHints()`, `applyPostCreateHints()`) for free.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the actual dispatcher method names in the migration guide.

This section references applyPreCreateHints() / applyPostCreateHints(), but the PR objective defines runPreCreateHints() / runPostCreateHints(). Keeping these names aligned avoids upgrade-time confusion.

📘 Proposed docs fix
- dispatchers (`applyPreCreateHints()`, `applyPostCreateHints()`) for free.
+ dispatchers (`runPreCreateHints()`, `runPostCreateHints()`) for free.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`registerCreationHint()`, `getCreationHints()`, and two protected
dispatchers (`applyPreCreateHints()`, `applyPostCreateHints()`) for free.
`registerCreationHint()`, `getCreationHints()`, and two protected
dispatchers (`runPreCreateHints()`, `runPostCreateHints()`) for free.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@UPGRADING.md` around lines 413 - 415, The migration guide currently
references the wrong dispatcher method names; update the text to use the actual
method names runPreCreateHints() and runPostCreateHints() instead of
applyPreCreateHints() / applyPostCreateHints() so the docs match the
implementation (also check nearby mentions of registerCreationHint() and
getCreationHints() to ensure consistency).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not adopting. The current implementation in CreationHintRegistryTrait uses applyPreCreateHints() and applyPostCreateHints(), so the UPGRADING.md text matches the code as shipped. Renaming to runPre/PostCreateHints() would make the documentation diverge from the actual method names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify actual method names in CreationHintRegistryTrait
rg -n "function apply|function run" --type=php src/Drupal/Driver/Hint/CreationHintRegistryTrait.php

Length of output: 217


@AlexSkrypnyk, you're absolutely right — the script confirms the trait exposes applyPreCreateHints() (line 64) and applyPostCreateHints() (line 88), so UPGRADING.md is accurate as written. My original comment was based on the PR objective description rather than the actual method names in the code. No change needed here, sorry for the noise!

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 (2)
tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php (2)

102-105: ⚡ Quick win

Coercion test name over-promises current coverage.

At Line 102, the provider only sends strings, so it doesn’t actually validate non-string coercion. Add at least one numeric case.

Proposed update
   public static function dataProviderApplyToStubCoercesValueToString(): iterable {
     yield 'plain string' => ['alice', 'alice'];
     yield 'integer-like string' => ['7', '7'];
+    yield 'integer' => [7, '7'];
+    yield 'float' => [7.5, '7.5'];
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php` around lines 102
- 105, The data provider dataProviderApplyToStubCoercesValueToString currently
only yields string inputs so the test name is misleading; add at least one
non-string numeric case (e.g., yield 'integer' => [7, '7']) so the test actually
verifies that applyToStub coerces numeric values to strings; update the provider
in AuthorHintTest::dataProviderApplyToStubCoercesValueToString to include this
numeric example alongside the existing string cases.

121-128: ⚡ Quick win

Assert lookup callback is not called for empty author values.

At Line 121, this test can be stricter by failing immediately if the resolver callback is invoked, proving the empty-value guard runs before lookup.

Proposed update
-    $hint = new AuthorHint(static fn (): object => new FakeUser(1));
+    $hint = new AuthorHint(static function (): object {
+      self::fail('Lookup callback must not run when author is empty.');
+      return new FakeUser(1);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php` around lines 121
- 128, Make the test ensure the AuthorHint resolver callback is NOT invoked for
empty author values by replacing the current resolver (static fn (): object =>
new FakeUser(1)) with a callback that will immediately fail or throw if
executed; keep the stub creation via EntityStub('node','article',['author' =>
$author]) and the existing expectException/expectExceptionMessageMatches
assertions, then call AuthorHint->applyToStub($stub) to verify the guard
prevents the lookup callback from being called.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php`:
- Around line 102-105: The data provider
dataProviderApplyToStubCoercesValueToString currently only yields string inputs
so the test name is misleading; add at least one non-string numeric case (e.g.,
yield 'integer' => [7, '7']) so the test actually verifies that applyToStub
coerces numeric values to strings; update the provider in
AuthorHintTest::dataProviderApplyToStubCoercesValueToString to include this
numeric example alongside the existing string cases.
- Around line 121-128: Make the test ensure the AuthorHint resolver callback is
NOT invoked for empty author values by replacing the current resolver (static fn
(): object => new FakeUser(1)) with a callback that will immediately fail or
throw if executed; keep the stub creation via
EntityStub('node','article',['author' => $author]) and the existing
expectException/expectExceptionMessageMatches assertions, then call
AuthorHint->applyToStub($stub) to verify the guard prevents the lookup callback
from being called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df0233f5-7ec2-45ae-8cd3-c10279c88941

📥 Commits

Reviewing files that changed from the base of the PR and between b76a8dc and 0405960.

📒 Files selected for processing (4)
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/AuthorHintTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/ParentTermHintTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Hint/VocabularyMachineNameHintTest.php
  • tests/Drupal/Tests/Driver/Unit/Hint/RolesHintTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Drupal/Tests/Driver/Unit/Hint/RolesHintTest.php

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.

Driver creation hints have no discoverability mechanism

1 participant