Skip to content

Refactored handler pipeline to template-method: single 'expand()', internal 'normalise()' and 'doExpand()'.#372

Merged
AlexSkrypnyk merged 3 commits into
masterfrom
feature/normalise-public
May 19, 2026
Merged

Refactored handler pipeline to template-method: single 'expand()', internal 'normalise()' and 'doExpand()'.#372
AlexSkrypnyk merged 3 commits into
masterfrom
feature/normalise-public

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors the field handler pipeline to a template-method shape and collapses every per-handler unit test to a single testExpand() against the public method.

FieldHandlerInterface keeps one public method: expand(mixed $values): array. AbstractHandler::expand() is now final and runs $this->doExpand($this->normalise($values)). normalise() is protected (default lives on AbstractHandler; subclasses override only for shorthand parsing). doExpand() is abstract protected and only ever sees canonical records. The discipline is enforced by the base class rather than by callers remembering to call normalise() first - there is no public path that bypasses it.

Changes

Interface and base class

  • FieldHandlerInterface declares only expand(mixed $values): array.
  • AbstractHandler::expand() is final; body is one line.
  • AbstractHandler::normalise() is protected. Default shape: folds scalars into [mainProperty => value] records, rejects mixed positional/named keys at the top level, rejects records missing the main property.
  • AbstractHandler::doExpand() is abstract protected.

Core::expandEntityFields()

One call per field: $stub->setValue($field, $handler->expand($value));.

Per-handler updates

Every concrete handler renames the previous public function expand(array $records) to protected function doExpand(array $records). Validation and casting move into normalise():

  • FileHandler / ImageHandler / SupportedImageHandler: NULL/empty target_id rejection and (string) cast live in normalise(). doExpand() only does file resolution / upload.
  • EntityReferenceHandler / EntityReferenceRevisionsHandler: doExpand() skips the entity-storage round-trip when the lookup is already an int. Numeric-string lookups go through the OR-condition (id_key = X OR label_key = 'X') so digit-only labels still resolve.
  • LinkHandler / NameHandler / SmartdateHandler keep their normalise() overrides for shorthand parsing.
  • AddressHandler / DaterangeHandler distinguish a single positional record from a list of records via an array_is_list && is_array($values[0]) check, so ['John', 'Doe'] and ['2026-07-15T09:00:00', '2026-07-15T17:00:00'] resolve as one record rather than multi-delta.
  • TimeHandler throws InvalidArgumentException when strtotime() returns FALSE.

Reference handlers consistently use $this->mainProperty instead of the literal 'target_id'.

Aliases

  • AuthorAlias casts the resolved uid to int so the int-only fast path in EntityReferenceHandler::doExpand() applies.
  • ParentTermAlias casts the resolved tid to int for the same reason.
  • RolesAlias unwraps target_id from the records that EntityReferenceHandler now produces, so the post-create role assignment still works after roles flows through the pipeline.

Tests

  • New FieldHandlerUnitTestBase declares a single testExpand() driven by an abstract dataProviderExpand(). Every concrete handler test extends it and supplies only createHandler() and the data rows.
  • ListHandlerTest split into ListStringHandlerTest, ListIntegerHandlerTest, ListFloatHandlerTest - one allowed-values configuration per file.
  • FileHandlerTest, ImageHandlerTest, SupportedImageHandlerTest use one setUp() that primes a universal Drupal container (URI-to-File-id index for the reuse branch, fresh File from file.repository for the upload branch). Unit-test rows exercise the upload, reuse, and rejection paths against a checked-in fixture at tests/fixtures/files/fixture.bin.
  • EntityReferenceHandlerTest, EntityReferenceRevisionsHandlerTest use one setUp() priming an entity_type.manager whose query stub matches a label-to-id index. Data rows cover label match, int passthrough, label miss.
  • Each data provider carries positive and negative rows inline using the standard [input, expected, exception_class, exception_message] shape.
  • Kernel tests for file / image / supported_image handlers use new fixtures at tests/fixtures/files/sample.txt (FileHandler) and tests/fixtures/files/sample.jpg (ImageHandler, SupportedImageHandler).
  • AbstractHandlerNormaliseTest retained - it exercises the base normalise() against a generic NormaliseTestHandler via reflection (the protected method is not on the public contract).

Comments

Class docblocks reduced to a single descriptive sentence. Method docblocks no longer describe how other methods relate to the one being documented.

Before / After

Public surface

BEFORE                                    AFTER
─────────────────────────────────         ─────────────────────────────────
interface FieldHandlerInterface {         interface FieldHandlerInterface {
  public function expand(                   public function expand(
    mixed $values                             mixed $values
  ): array;                                 ): array;
}                                         }

// Each handler implemented                // AbstractHandler::expand() is
// shape coercion + transformation         // final and template-methods:
// inside its own expand().
                                          final public function expand(
                                            mixed $values
                                          ): array {
                                            return $this->doExpand(
                                              $this->normalise($values)
                                            );
                                          }

                                          protected function normalise(
                                            mixed $values
                                          ): array { ... }

                                          abstract protected function doExpand(
                                            array $records
                                          ): array;

Test layout

BEFORE                                  AFTER
──────                                  ──────
Per-handler tests (heterogeneous):      Per-handler tests (uniform):
  - testExpand() (some)                   - createHandler()
  - testNormalise() (some)                - dataProviderExpand()
  - ad-hoc test methods                   - (nothing else)
  - mock setup duplicated
                                        FieldHandlerUnitTestBase:
                                          - testExpand(
                                              $input,
                                              $expected,
                                              $exception,
                                              $message
                                            )
                                        Rows carry positive and
                                        negative variants inline.

Summary by CodeRabbit

  • Refactoring
    • Restructured field handling architecture for improved consistency and maintainability.
    • Enhanced input validation with stricter type checking across multiple field types.
    • Improved error messages for invalid field inputs and data formats.
    • Updated documentation for field handling behavior and expectations.
    • Consolidated test infrastructure for better test organization.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: 4756556f-5c11-4f2d-be49-e2f58457ccda

📥 Commits

Reviewing files that changed from the base of the PR and between a2dda76 and dc6c1b8.

📒 Files selected for processing (4)
  • src/Drupal/Driver/Core/Alias/AuthorAlias.php
  • src/Drupal/Driver/Core/Alias/ParentTermAlias.php
  • src/Drupal/Driver/Core/Field/AddressHandler.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php
📝 Walkthrough

<review_stack_artifact>

</review_stack_artifact>

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

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown

Code coverage (threshold: 95%)



Code Coverage Report Summary:
  Classes: 58.82% (20/34)
  Methods: 89.63% (216/241)
  Lines:   96.34% (1212/1258)

Per-class coverage
Drupal\Driver\Alias\CreationAliasRegistryTrait               100.00%
Drupal\Driver\Alias\RolesAlias                                87.50%
Drupal\Driver\BlackboxDriver                                 100.00%
Drupal\Driver\Core\Alias\AuthorAlias                          90.00%
Drupal\Driver\Core\Alias\ParentTermAlias                      88.24%
Drupal\Driver\Core\Alias\VocabularyMachineNameAlias           88.89%
Drupal\Driver\Core\Core                                       96.06%
Drupal\Driver\Core\Field\AbstractHandler                      95.74%
Drupal\Driver\Core\Field\AddressHandler                       94.83%
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               94.44%
Drupal\Driver\Core\Field\EntityReferenceRevisionsHandler      86.05%
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                          94.00%
Drupal\Driver\Core\Field\ListFloatHandler                    100.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                          96.36%
Drupal\Driver\Core\Field\OgStandardReferenceHandler            0.00%
Drupal\Driver\Core\Field\SmartdateHandler                    100.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                          91.67%
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\CreationAliasResolutionException       0.00%
Drupal\Driver\Exception\Exception                            100.00%
Drupal\Driver\Exception\UnsupportedDriverActionException     100.00%

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 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/Field/AddressHandler.php`:
- Around line 52-60: In expand(), the code uses
reset($this->fieldConfig->getSettings()['available_countries']) which returns
FALSE when available_countries is empty; update expand(array $records): array to
guard that case: fetch $available =
$this->fieldConfig->getSettings()['available_countries'] (or cast to array),
check if non-empty before using reset(), and if empty set a sensible default
(e.g. empty string or null) for $record['country_code'] instead of boolean false
so country_code is always a string/nullable value; modify the logic inside
expand() accordingly to use the guarded value.

In `@src/Drupal/Driver/Core/Field/FileHandler.php`:
- Around line 22-24: The loop in FileHandler.php dereferences
$record['target_id'] without validation which can cause undefined-key notices;
update the foreach that builds $file_path to first check that 'target_id' exists
and is non-empty (e.g. isset and non-empty string after casting/trimming) and if
not throw a clear InvalidArgumentException (or a domain-specific exception)
before calling resolveExistingFile or uploadAndSave; ensure
resolveExistingFile($file_path) and uploadAndSave($file_path) are only invoked
with a validated string to fail fast and avoid downstream file-read errors.

In `@src/Drupal/Driver/Core/Field/ImageHandler.php`:
- Around line 25-30: The validation currently checks for NULL/empty but doesn't
handle a missing 'target_id' key; update the guard in ImageHandler so you first
verify the key exists (e.g. using array_key_exists('target_id', $record) or
!isset($record['target_id'])) and then check for null/empty, and throw the same
InvalidArgumentException referencing $this->mainProperty when the key is absent
or invalid; after that safely cast $record['target_id'] to string and continue
to call $this->resolveExistingFile($file_path) ??
$this->uploadAndSave($file_path).

In `@src/Drupal/Driver/Core/Field/SupportedImageHandler.php`:
- Around line 22-28: The code dereferences $record['target_id'] without
validation and throws a generic exception on read failure; update the block in
SupportedImageHandler (the code using $record['target_id'], $file_path and
file_get_contents) to first validate that $record['target_id'] exists and is
non-empty (throw a clear exception referencing missing target_id), then check
file existence/readability (e.g., is_readable($file_path)) before calling
file_get_contents, and if reading fails include the actual $file_path (and any
underlying error details) in the thrown Exception to improve diagnostics.
🪄 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: b58555e2-7686-4a1c-a1ca-cd537e778fce

📥 Commits

Reviewing files that changed from the base of the PR and between d07fa7d and 572eb6c.

📒 Files selected for processing (45)
  • src/Drupal/Driver/Alias/RolesAlias.php
  • src/Drupal/Driver/Core/Core.php
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
  • src/Drupal/Driver/Core/Field/AddressHandler.php
  • src/Drupal/Driver/Core/Field/BooleanHandler.php
  • src/Drupal/Driver/Core/Field/DaterangeHandler.php
  • src/Drupal/Driver/Core/Field/DatetimeHandler.php
  • src/Drupal/Driver/Core/Field/DefaultHandler.php
  • src/Drupal/Driver/Core/Field/EmbridgeAssetItemHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceRevisionsHandler.php
  • src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
  • src/Drupal/Driver/Core/Field/FileHandler.php
  • src/Drupal/Driver/Core/Field/ImageHandler.php
  • src/Drupal/Driver/Core/Field/LinkHandler.php
  • src/Drupal/Driver/Core/Field/ListHandlerBase.php
  • src/Drupal/Driver/Core/Field/NameHandler.php
  • src/Drupal/Driver/Core/Field/OgStandardReferenceHandler.php
  • src/Drupal/Driver/Core/Field/SmartdateHandler.php
  • src/Drupal/Driver/Core/Field/SupportedImageHandler.php
  • src/Drupal/Driver/Core/Field/TextHandler.php
  • src/Drupal/Driver/Core/Field/TextLongHandler.php
  • src/Drupal/Driver/Core/Field/TextWithSummaryHandler.php
  • src/Drupal/Driver/Core/Field/TimeHandler.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AbstractHandlerNormaliseTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AddressHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/BooleanHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DaterangeHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DatetimeHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DefaultHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceRevisionsHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FileHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/LinkHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/NameHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/SmartdateHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/SupportedImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextLongHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextWithSummaryHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TimeHandlerTest.php

Comment thread src/Drupal/Driver/Core/Field/AddressHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/FileHandler.php
Comment thread src/Drupal/Driver/Core/Field/ImageHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/SupportedImageHandler.php Outdated
@AlexSkrypnyk AlexSkrypnyk changed the title Promoted 'normalise()' to public field handler contract called by 'Core'. Refactored field handlers to template-method pipeline with internal 'normalise()' and 'doExpand()'. May 19, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

♻️ Duplicate comments (2)
src/Drupal/Driver/Core/Field/FileHandler.php (1)

18-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Read the main property with ?? before validating it.

If a keyed record omits $this->mainProperty, line 19 raises an undefined-array-key notice before your InvalidArgumentException can fire. Malformed file records should fail cleanly here.

Suggested fix
     foreach ($records as &$record) {
-      if ($record[$this->mainProperty] === NULL || $record[$this->mainProperty] === '') {
+      $target = $record[$this->mainProperty] ?? NULL;
+      if ($target === NULL || $target === '') {
         throw new \InvalidArgumentException(sprintf('%s field "%s" must not be NULL or empty.', $this->getFieldLabel(), $this->mainProperty));
       }

-      $record[$this->mainProperty] = (string) $record[$this->mainProperty];
+      $record[$this->mainProperty] = (string) $target;
     }
🤖 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/Field/FileHandler.php` around lines 18 - 24, The loop
over $records reads $record[$this->mainProperty] directly which can trigger an
undefined-array-key notice; first read the value with the null-coalescing
operator (e.g. $value = $record[$this->mainProperty] ?? null) and validate
$value for NULL or empty before throwing the \InvalidArgumentException (use
$this->getFieldLabel() and $this->mainProperty in the message as before); then
set $record[$this->mainProperty] = (string)$value so missing keys are handled
cleanly and the exception only fires for malformed file records.
src/Drupal/Driver/Core/Field/SupportedImageHandler.php (1)

17-25: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Keep normalise() public and restore the guarded main-property read.

Core now calls handler->normalise() directly, so narrowing this override to protected breaks the new contract. The direct $record[$this->mainProperty] access also reintroduces the undefined-key path instead of consistently throwing the intended InvalidArgumentException for malformed records.

Proposed fix
-  protected function normalise(mixed $values): array {
+  public function normalise(mixed $values): array {
     $records = parent::normalise($values);

     foreach ($records as &$record) {
-      if ($record[$this->mainProperty] === NULL || $record[$this->mainProperty] === '') {
+      $value = $record[$this->mainProperty] ?? NULL;
+      if ($value === NULL || $value === '') {
         throw new \InvalidArgumentException(sprintf('Supported image field "%s" must not be NULL or empty.', $this->mainProperty));
       }

-      $record[$this->mainProperty] = (string) $record[$this->mainProperty];
+      $record[$this->mainProperty] = (string) $value;
     }
#!/bin/bash
# Verify the normalise() visibility contract across the handler hierarchy.
rg -nP '\b(public|protected)\s+function\s+normalise\s*\(' \
  src/Drupal/Driver/Core/Field/AbstractHandler.php \
  src/Drupal/Driver/Core/Field/SupportedImageHandler.php

# Show the interface declaration that Core is expected to call.
rg -nP '\bfunction\s+normalise\s*\(' \
  src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
🤖 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/Field/SupportedImageHandler.php` around lines 17 - 25,
The override of normalise() in SupportedImageHandler is incorrectly declared
protected and directly indexes $record[$this->mainProperty], risking
undefined-key access; change the method signature back to public to match Core's
contract (public function normalise(mixed $values): array) and, inside the loop
after calling parent::normalise($values), replace direct indexing with a guarded
read that checks array_key_exists($this->mainProperty, $record) and that the
value is not null/empty, throwing the existing InvalidArgumentException when the
key is missing or the value is NULL/empty, then cast the value to string and
assign back to $record[$this->mainProperty].
🤖 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/Field/AddressHandler.php`:
- Around line 30-40: The current branch naively expands any top-level list into
multiple records, which splits positional addresses like ['John','Smith','123
Main St'] into three deltas; change the logic in AddressHandler so that when
array_is_list($values) you first detect whether the list is actually a single
positional address (e.g., elements are scalars or each element is itself a
positional list) vs a list of associative record arrays, and only iterate into
multiple records when the elements are associative record arrays; otherwise call
$this->normaliseDelta($values, $visible_fields) once and return that single
record. Use the existing normaliseDelta method and the $visible_fields variable
to decide (for example, check is_array($el) && !array_is_list($el) or presence
of string keys on elements) to distinguish positional single-address lists from
lists of records.

In `@src/Drupal/Driver/Core/Field/DaterangeHandler.php`:
- Around line 22-40: The code in DaterangeHandler that normalises $values treats
any PHP list as multiple records and thus rejects the shorthand single-record
positional form [start, end]; modify the normalisation logic around $values so
that if $values is an array_is_list and its elements are scalars (or not arrays)
— e.g. count($values) === 2 and !is_array($values[0]) — you wrap it as a single
record (i.e. $values = [$values]) before iterating; keep the existing behaviour
for a list of record arrays and for non-list inputs so the foreach building
$records with keys 'value' and 'end_value' still works.

In `@src/Drupal/Driver/Core/Field/EntityReferenceHandler.php`:
- Around line 18-23: The loop in EntityReferenceHandler that coerces digit-only
string labels into integers (the block modifying $record[$this->mainProperty]
when is_string($lookup) && ctype_digit($lookup)) causes numeric-looking labels
like "123" to be treated as pre-resolved IDs and thus skips the lookup and
bundle/not-found checks; remove this coercion (or preserve the original string
type) so that lookup logic still runs for digit-only labels and ensure the same
change is applied to the similar block around the other occurrence (lines that
check ctype_digit) to avoid bypassing label resolution and validation.

In `@src/Drupal/Driver/Core/Field/EntityReferenceRevisionsHandler.php`:
- Around line 20-25: The integer fast-path in the foreach (casting
$record[$this->mainProperty] to int when is_string && ctype_digit) can create
dangling revision refs; instead of coercing early, remove or guard the cast so
numeric-looking labels still go through the normal validation/load path: defer
any integer conversion for $this->mainProperty until after you validate the
entity/bundle and successfully load the target revision (the code paths around
the query/bundle validation and revision load that write target_revision_id must
be reached); if the revision/entity cannot be loaded, do not write a numeric
target_revision_id (leave original value or set to null) and ensure the
subsequent logic that sets target_revision_id only runs on successful loads
(affecting the code that references $records, $this->mainProperty,
target_revision_id and the revision load/validation logic).

In `@src/Drupal/Driver/Core/Field/FieldHandlerInterface.php`:
- Around line 7-9: Update the FieldHandlerInterface class-level docblock to
describe the two-stage expansion pipeline: explain that implementations must
first accept raw input via normalise(mixed $values) to convert and validate
values into a canonical intermediate form, and then perform batch expansion via
doExpand(array $records) which takes the normalized records and returns
expanded/denormalized results; mention the contract guarantees (e.g., normalise
is called per-value before doExpand, doExpand may operate on arrays/batches and
should be idempotent) and reference the method names normalise and doExpand so
implementers can locate the expected flow.

In `@src/Drupal/Driver/Core/Field/LinkHandler.php`:
- Around line 45-79: The handler misinterprets a top-level positional tuple like
['Title','entity:node/1','foo=bar'] as multiple URI-only records; add a
disambiguation before the current normalization so that when $values is a list
(array_is_list($values)), has more than one item, and its elements are all
scalars (none are arrays), treat it as a single positional record by wrapping
it: $values = [$values]; update LinkHandler::the processing block (the code
around the array_is_list check and foreach over $values) and add tests covering
scalar-only list tuples vs multi-value scalar URI lists.

In `@src/Drupal/Driver/Core/Field/TimeHandler.php`:
- Around line 22-27: The code in TimeHandler.php currently pushes
strtotime((string)$value) - $midnight into $seconds even when strtotime returns
FALSE, producing large negative values; update the handling around the strtotime
call in the method (look for the $seconds array, $value and $midnight usages) to
first call strtotime((string)$value), check for strict !== FALSE, and only
subtract $midnight and push to $seconds when valid; for invalid/false results
reject or surface the error (throw/return/validation failure) instead of writing
the erroneous computed value.

---

Duplicate comments:
In `@src/Drupal/Driver/Core/Field/FileHandler.php`:
- Around line 18-24: The loop over $records reads $record[$this->mainProperty]
directly which can trigger an undefined-array-key notice; first read the value
with the null-coalescing operator (e.g. $value = $record[$this->mainProperty] ??
null) and validate $value for NULL or empty before throwing the
\InvalidArgumentException (use $this->getFieldLabel() and $this->mainProperty in
the message as before); then set $record[$this->mainProperty] = (string)$value
so missing keys are handled cleanly and the exception only fires for malformed
file records.

In `@src/Drupal/Driver/Core/Field/SupportedImageHandler.php`:
- Around line 17-25: The override of normalise() in SupportedImageHandler is
incorrectly declared protected and directly indexes
$record[$this->mainProperty], risking undefined-key access; change the method
signature back to public to match Core's contract (public function
normalise(mixed $values): array) and, inside the loop after calling
parent::normalise($values), replace direct indexing with a guarded read that
checks array_key_exists($this->mainProperty, $record) and that the value is not
null/empty, throwing the existing InvalidArgumentException when the key is
missing or the value is NULL/empty, then cast the value to string and assign
back to $record[$this->mainProperty].
🪄 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: 4587f5ab-6033-4991-a9b9-e39777c450f6

📥 Commits

Reviewing files that changed from the base of the PR and between 572eb6c and 2f92f3b.

📒 Files selected for processing (34)
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
  • src/Drupal/Driver/Core/Field/AddressHandler.php
  • src/Drupal/Driver/Core/Field/BooleanHandler.php
  • src/Drupal/Driver/Core/Field/DaterangeHandler.php
  • src/Drupal/Driver/Core/Field/DatetimeHandler.php
  • src/Drupal/Driver/Core/Field/DefaultHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceRevisionsHandler.php
  • src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
  • src/Drupal/Driver/Core/Field/FileHandler.php
  • src/Drupal/Driver/Core/Field/ImageHandler.php
  • src/Drupal/Driver/Core/Field/LinkHandler.php
  • src/Drupal/Driver/Core/Field/ListHandlerBase.php
  • src/Drupal/Driver/Core/Field/NameHandler.php
  • src/Drupal/Driver/Core/Field/SmartdateHandler.php
  • src/Drupal/Driver/Core/Field/SupportedImageHandler.php
  • src/Drupal/Driver/Core/Field/TextHandler.php
  • src/Drupal/Driver/Core/Field/TextLongHandler.php
  • src/Drupal/Driver/Core/Field/TextWithSummaryHandler.php
  • src/Drupal/Driver/Core/Field/TimeHandler.php
  • tests/Drupal/Tests/Driver/Kernel/Core/Field/FieldHandlerRegistryKernelTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/CoreFieldHandlerLookupTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AbstractHandlerNormaliseTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AddressHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DefaultHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceRevisionsHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FileHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/NameHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/SupportedImageHandlerTest.php
  • tests/fixtures/ConsumerProject/Driver/Field/StringLongHandler.php
  • tests/fixtures/ConsumerProject/Driver/Field/TextLongHandler.php
✅ Files skipped from review due to trivial changes (1)
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/Drupal/Driver/Core/Field/DefaultHandler.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DefaultHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AddressHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/NameHandlerTest.php

Comment thread src/Drupal/Driver/Core/Field/AddressHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/DaterangeHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/EntityReferenceHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/EntityReferenceRevisionsHandler.php Outdated
Comment thread src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
Comment thread src/Drupal/Driver/Core/Field/LinkHandler.php
Comment thread src/Drupal/Driver/Core/Field/TimeHandler.php Outdated
@AlexSkrypnyk AlexSkrypnyk changed the title Refactored field handlers to template-method pipeline with internal 'normalise()' and 'doExpand()'. Refactored field handler pipeline to template method and collapsed handler tests to single 'testExpand'. May 19, 2026
@AlexSkrypnyk AlexSkrypnyk changed the title Refactored field handler pipeline to template method and collapsed handler tests to single 'testExpand'. Refactored handler pipeline to template-method: single 'expand()', internal 'normalise()' and 'doExpand()'. May 19, 2026
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/normalise-public branch from b3e410f to 85989ca Compare May 19, 2026 00:55
@AlexSkrypnyk

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

…ive guards on reference handlers, NULL load throw.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/Drupal/Driver/Core/Field/NameHandler.php (1)

58-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject unsupported list item types instead of silently dropping them.

At Line 58-67, non-string/non-array items are ignored. That can silently lose records and mask invalid input; throw an exception for unsupported element types.

Suggested fix
     foreach ($values as $value) {
       if (is_string($value)) {
         $names[] = $this->normaliseString($value, $enabled);
         continue;
       }

       if (is_array($value)) {
         $names[] = $this->normaliseArray($value, $enabled);
+        continue;
       }
+
+      throw new \InvalidArgumentException(sprintf(
+        'Name field record must be a string or array; got %s.',
+        get_debug_type($value),
+      ));
     }
🤖 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/Field/NameHandler.php` around lines 58 - 67, The loop
in NameHandler that iterates over $values currently drops items that are neither
strings nor arrays, which can silently lose data; update the foreach block (the
loop that calls normaliseString() and normaliseArray()) to throw an
InvalidArgumentException for any unsupported item type instead of ignoring it,
including in the exception message the offending item's type (and optionally its
index or key) and the field/context so callers can diagnose invalid input; keep
calls to normaliseString() and normaliseArray() as-is for string/array items.
🤖 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/Alias/AuthorAlias.php`:
- Around line 85-87: Validate the resolved user id returned by $user->id()
before coercing to int: ensure it's numeric and greater than zero (e.g.,
is_numeric and > 0) and only then call $stub->setValue('uid', (int)
$user->id()); otherwise avoid assigning the uid (or raise/return an explicit
error) so you don't silently convert invalid values to 0; update the logic in
AuthorAlias.php around the $stub->setValue('uid', ...) call to perform this
guard.

In `@src/Drupal/Driver/Core/Alias/ParentTermAlias.php`:
- Around line 117-119: Guard against invalid tid before casting in
ParentTermAlias: verify the $tid returned by your lookup is a numeric positive
ID (e.g., is_numeric and > 0) before calling $stub->setValue('parent', (int)
$tid); if the check fails, do not set the parent value and instead handle the
error (throw/return/skip) so you don't silently convert unexpected strings to
0—update the logic around the lookup and the $stub->setValue('parent', (int)
$tid) call to perform this validation.

In `@src/Drupal/Driver/Core/Field/AddressHandler.php`:
- Around line 41-43: The foreach over $values in AddressHandler is passing
non-array/non-string items into normaliseDelta which can cause malformed
records; update the loop that builds $records to validate each $value is an
array or string before calling AddressHandler::normaliseDelta (use
is_array/is_string), and if not, throw an InvalidArgumentException (include the
offending value/index and a clear message referencing the field being processed)
so callers get a clear error instead of silently producing bad records.

In `@tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php`:
- Around line 59-63: The test currently always suppresses PHP warnings by
calling @$handler->expand($input), which can hide issues on non-exception rows;
update the test in FieldHandlerUnitTestBase so that you only prepend the
error-suppression operator when the current test row expects an exception (e.g.,
check the row's metadata / a flag like 'exception' or similar used by the data
provider), otherwise call $handler->expand($input) without '@', and keep
asserting the exception behavior unchanged for exception rows.

---

Outside diff comments:
In `@src/Drupal/Driver/Core/Field/NameHandler.php`:
- Around line 58-67: The loop in NameHandler that iterates over $values
currently drops items that are neither strings nor arrays, which can silently
lose data; update the foreach block (the loop that calls normaliseString() and
normaliseArray()) to throw an InvalidArgumentException for any unsupported item
type instead of ignoring it, including in the exception message the offending
item's type (and optionally its index or key) and the field/context so callers
can diagnose invalid input; keep calls to normaliseString() and normaliseArray()
as-is for string/array items.
🪄 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: 03be3628-6512-49d8-8c87-2e080af638a5

📥 Commits

Reviewing files that changed from the base of the PR and between 2f92f3b and a2dda76.

⛔ Files ignored due to path filters (1)
  • tests/fixtures/files/fixture.bin is excluded by !**/*.bin
📒 Files selected for processing (54)
  • src/Drupal/Driver/Alias/RolesAlias.php
  • src/Drupal/Driver/Core/Alias/AuthorAlias.php
  • src/Drupal/Driver/Core/Alias/ParentTermAlias.php
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
  • src/Drupal/Driver/Core/Field/AddressHandler.php
  • src/Drupal/Driver/Core/Field/BooleanHandler.php
  • src/Drupal/Driver/Core/Field/DaterangeHandler.php
  • src/Drupal/Driver/Core/Field/DatetimeHandler.php
  • src/Drupal/Driver/Core/Field/DefaultHandler.php
  • src/Drupal/Driver/Core/Field/EmbridgeAssetItemHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceHandler.php
  • src/Drupal/Driver/Core/Field/EntityReferenceRevisionsHandler.php
  • src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
  • src/Drupal/Driver/Core/Field/FileHandler.php
  • src/Drupal/Driver/Core/Field/ImageHandler.php
  • src/Drupal/Driver/Core/Field/LinkHandler.php
  • src/Drupal/Driver/Core/Field/ListFloatHandler.php
  • src/Drupal/Driver/Core/Field/ListHandlerBase.php
  • src/Drupal/Driver/Core/Field/NameHandler.php
  • src/Drupal/Driver/Core/Field/OgStandardReferenceHandler.php
  • src/Drupal/Driver/Core/Field/SmartdateHandler.php
  • src/Drupal/Driver/Core/Field/SupportedImageHandler.php
  • src/Drupal/Driver/Core/Field/TextHandler.php
  • src/Drupal/Driver/Core/Field/TextLongHandler.php
  • src/Drupal/Driver/Core/Field/TextWithSummaryHandler.php
  • src/Drupal/Driver/Core/Field/TimeHandler.php
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
  • tests/Drupal/Tests/Driver/Kernel/Core/Field/FieldHandlerRegistryKernelTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/CoreFieldHandlerLookupTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AbstractHandlerNormaliseTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AddressHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/BooleanHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DaterangeHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DatetimeHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/DefaultHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/EntityReferenceRevisionsHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/FileHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/LinkHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListFloatHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListIntegerHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListStringHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/NameHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/SmartdateHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/SupportedImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextLongHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TextWithSummaryHandlerTest.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/TimeHandlerTest.php
  • tests/fixtures/ConsumerProject/Driver/Field/StringLongHandler.php
  • tests/fixtures/ConsumerProject/Driver/Field/TextLongHandler.php
💤 Files with no reviewable changes (1)
  • tests/Drupal/Tests/Driver/Unit/Core/Field/ListHandlerTest.php
✅ Files skipped from review due to trivial changes (4)
  • src/Drupal/Driver/Core/Field/EmbridgeAssetItemHandler.php
  • src/Drupal/Driver/Core/Field/OgStandardReferenceHandler.php
  • src/Drupal/Driver/Core/Field/FieldHandlerInterface.php
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
🚧 Files skipped from review as they are similar to previous changes (17)
  • tests/Drupal/Tests/Driver/Kernel/Core/CoreEntityMethodsKernelTest.php
  • src/Drupal/Driver/Core/Field/DefaultHandler.php
  • src/Drupal/Driver/Alias/RolesAlias.php
  • src/Drupal/Driver/Core/Field/TextLongHandler.php
  • src/Drupal/Driver/Core/Field/TextWithSummaryHandler.php
  • tests/Drupal/Tests/Driver/Kernel/Core/Field/FieldHandlerRegistryKernelTest.php
  • src/Drupal/Driver/Core/Field/DatetimeHandler.php
  • src/Drupal/Driver/Core/Field/ListHandlerBase.php
  • tests/fixtures/ConsumerProject/Driver/Field/TextLongHandler.php
  • src/Drupal/Driver/Core/Field/TimeHandler.php
  • src/Drupal/Driver/Core/Field/TextHandler.php
  • src/Drupal/Driver/Core/Field/SupportedImageHandler.php
  • src/Drupal/Driver/Core/Field/LinkHandler.php
  • tests/Drupal/Tests/Driver/Unit/Core/Field/AbstractHandlerNormaliseTest.php
  • src/Drupal/Driver/Core/Field/SmartdateHandler.php
  • src/Drupal/Driver/Core/Field/BooleanHandler.php
  • src/Drupal/Driver/Core/Field/FileHandler.php

Comment thread src/Drupal/Driver/Core/Alias/AuthorAlias.php Outdated
Comment thread src/Drupal/Driver/Core/Alias/ParentTermAlias.php
Comment thread src/Drupal/Driver/Core/Field/AddressHandler.php
Comment thread tests/Drupal/Tests/Driver/Unit/Core/Field/FieldHandlerUnitTestBase.php Outdated
@AlexSkrypnyk AlexSkrypnyk merged commit e070e1a into master May 19, 2026
13 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/normalise-public branch May 19, 2026 04:44
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.

1 participant