Skip to content

Restructured cores into 'Core/' with version-override lookup chain.#338

Merged
AlexSkrypnyk merged 6 commits intomasterfrom
feature/restructure
Apr 17, 2026
Merged

Restructured cores into 'Core/' with version-override lookup chain.#338
AlexSkrypnyk merged 6 commits intomasterfrom
feature/restructure

Conversation

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

@AlexSkrypnyk AlexSkrypnyk commented Apr 17, 2026

Note: This PR is part of the v3.x upgrade. See the
3.x epic #312
for the full scope. The architectural pattern introduced here is
documented in doc/extending.rst.

Part of #312

Summary

Restructures the source layout so each Drupal version can override behavior cleanly without copy-pasting the entire implementation. The old Cores/Drupal8.php (which actually served D8-D12+) is renamed to Core/Core.php, the abstract Core layer is consolidated under Core/, and field handlers move next to the Core they belong to (Core/Field/). Version-specific overrides live in Core{N}/ directories (e.g., Core12/) - these don't exist today; the lookup chain in DrupalDriver::setCoreFromVersion() and AbstractCore::getFieldHandler() falls back to the default Core\Core and Core\Field\* when no override is registered.

Changes

Documentation

  • Added doc/extending.rst explaining the Core layer, the lookup chain, and how to add per-version customizations.
  • doc/index.rst toctree updated.

Source restructure

  • src/Drupal/Driver/Cores/ -> src/Drupal/Driver/Core/ (singular).
  • src/Drupal/Driver/Cores/Drupal8.php -> src/Drupal/Driver/Core/Core.php. Class renamed from Drupal8 to Core.
  • src/Drupal/Driver/Fields/Drupal8/*.php (17 files) -> src/Drupal/Driver/Core/Field/*.php.
  • src/Drupal/Driver/Fields/FieldHandlerInterface.php -> src/Drupal/Driver/Core/Field/FieldHandlerInterface.php.
  • All file moves done via git mv to preserve history.

Lookup chain

  • DrupalDriver::setCoreFromVersion(): walks Drupal\Driver\Core{N}\Core from the detected major version down to Drupal\Driver\Core\Core. First existing class wins.
  • AbstractCore::getFieldHandler(): walks Drupal\Driver\Core{N}\Field\{X}Handler similarly, falling back to Drupal\Driver\Core\Field\{X}Handler and then to DefaultHandler candidates.
  • New AbstractCore::getVersion() method (returns 0 by default; future per-version Core{N}\Core classes override it to return their version).
  • DrupalDriver::detectMajorVersion() now returns the actual Drupal major version (10, 11, 12) instead of always returning 8.

Tests + spec

  • Test files mirroring Fields/Drupal8/ moved to Core/Field/ (13 files).
  • Top-level Drupal8Test, Drupal8FieldMethodsTest, Drupal8PermissionsTest kept at root; namespaces and references to old class names updated; internal helper classes renamed (TestDrupal8Core -> TestCore).
  • spec/Drupal/Driver/Cores/Drupal8Spec.php -> spec/Drupal/Driver/Core/CoreSpec.php. Class renamed.

Config

  • phpcs.xml exclude-pattern path updated from src/Drupal/Driver/Cores/*.php to src/Drupal/Driver/Core/*.php.

Before

src/Drupal/Driver/
├── DriverInterface.php
├── AuthenticationDriverInterface.php
├── SubDriverFinderInterface.php
├── BaseDriver.php
├── BlackboxDriver.php
├── DrupalDriver.php
├── DrushDriver.php
├── Cores/
│   ├── CoreInterface.php
│   ├── CoreAuthenticationInterface.php
│   ├── AbstractCore.php
│   └── Drupal8.php                  ◄ misnamed: actually serves D10/D11/D12+
├── Fields/
│   ├── FieldHandlerInterface.php
│   └── Drupal8/
│       ├── AbstractHandler.php
│       └── (16 handlers)            ◄ used by all Drupal versions
└── Exception/

After (this PR)

src/Drupal/Driver/
├── DriverInterface.php
├── AuthenticationDriverInterface.php
├── SubDriverFinderInterface.php
├── BaseDriver.php
├── BlackboxDriver.php
├── DrupalDriver.php
├── DrushDriver.php
├── Core/                            ◄ abstractions + default Drupal implementation
│   ├── CoreInterface.php
│   ├── CoreAuthenticationInterface.php
│   ├── AbstractCore.php
│   ├── Core.php                     ◄ default Drupal core (was Drupal8.php)
│   └── Field/
│       ├── FieldHandlerInterface.php
│       ├── AbstractHandler.php
│       └── (16 handlers)            ◄ default handler set
└── Exception/

No Core{N}/ directories exist yet. They are not needed today - all currently-supported Drupal versions use the default in Core/.

Future (when a Drupal version diverges)

When Drupal 12 ships and one handler needs different behavior:

src/Drupal/Driver/
├── ... (unchanged)
├── Core/                            ◄ still the default for all versions
│   ├── CoreInterface.php
│   ├── AbstractCore.php
│   ├── Core.php
│   └── Field/
│       ├── FieldHandlerInterface.php
│       ├── AbstractHandler.php
│       └── (16 handlers)
│
├── Core12/                          ◄ ONLY files that differ in D12
│   └── Field/
│       └── FileHandler.php          ◄ extends Core\Field\FileHandler
│
└── Exception/

The Core12/Field/FileHandler.php extends the default and overrides only what changed:

namespace Drupal\Driver\Core12\Field;

use Drupal\Driver\Core\Field\FileHandler as DefaultFileHandler;

class FileHandler extends DefaultFileHandler {
  public function expand(mixed $values): array {
    // D12-specific behavior; delegate to parent for the common path.
    return parent::expand($values);
  }
}

Adding a new Drupal version (D13) that diverges further:

src/Drupal/Driver/
├── Core/                            ◄ default (covers anything not overridden below)
├── Core12/
│   └── Field/
│       └── FileHandler.php          ◄ first overridden in D12, applies to D13 too
└── Core13/
    ├── Core.php                     ◄ D13 needs Core-level changes
    └── Field/
        └── EntityReferenceHandler.php

The lookup chain at runtime for D13:

Resolving the Core class for Drupal 13:
  Try Drupal\Driver\Core13\Core              ◄ exists, use it
  Try Drupal\Driver\Core12\Core              (skipped - already found)
  Try Drupal\Driver\Core\Core                (skipped)

Resolving FileHandler for Drupal 13:
  Try Drupal\Driver\Core13\Field\FileHandler ◄ does not exist
  Try Drupal\Driver\Core12\Field\FileHandler ◄ EXISTS, use it
  Try Drupal\Driver\Core\Field\FileHandler   (skipped)

Resolving AddressHandler for Drupal 13:
  Try Drupal\Driver\Core13\Field\AddressHandler ◄ does not exist
  Try Drupal\Driver\Core12\Field\AddressHandler ◄ does not exist
  Try Drupal\Driver\Core\Field\AddressHandler   ◄ EXISTS, use it

The pattern keeps overrides local to the version that introduced them and avoids duplicating the entire handler set per Drupal version. New Drupal versions that don't diverge cost zero new files. See doc/extending.rst for the full guide.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced multi-version support with automatic handler lookup chain that selects version-specific implementations when available
    • Added framework for version-specific customizations without modifying core behavior
  • Documentation

    • New guide explaining driver extension strategy and multi-version support
  • Tests

    • Added comprehensive test coverage for version-specific handler resolution and core class lookup

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b456ec75-d756-4946-a27b-0b1aa0f9d19c

📥 Commits

Reviewing files that changed from the base of the PR and between b6e6585 and 36cd433.

📒 Files selected for processing (12)
  • CONTRIBUTING.md
  • doc/_static/snippets/usage-drupal.php
  • doc/extending.rst
  • tests/Drupal/Tests/Driver/Core/CoreFieldMethodsTest.php
  • tests/Drupal/Tests/Driver/Core/CorePermissionsTest.php
  • tests/Drupal/Tests/Driver/Core/CoreTest.php
  • tests/Drupal/Tests/Driver/Core/Field/LinkHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/NameHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/TimeHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/FieldHandlerLookupTest.php
  • tests/fixtures/Drupal/Driver/Core99/Core.php
  • tests/fixtures/Drupal/Driver/Core99/Field/FileHandler.php
💤 Files with no reviewable changes (1)
  • doc/_static/snippets/usage-drupal.php
✅ Files skipped from review due to trivial changes (3)
  • CONTRIBUTING.md
  • tests/fixtures/Drupal/Driver/Core99/Core.php
  • doc/extending.rst
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/fixtures/Drupal/Driver/Core99/Field/FileHandler.php

📝 Walkthrough

Walkthrough

This PR refactors the DrupalDriver codebase from a version-specific namespace structure (Drupal\Driver\Cores\Drupal8) to a generic, version-agnostic architecture (Drupal\Driver\Core) supporting per-major-version overrides. The field handler resolution logic is rewritten to implement a deterministic version-specific lookup chain that prioritizes version-specific implementations and falls back through older overrides to the default.

Changes

Cohort / File(s) Summary
Core namespace refactoring
`src/Drupal/Driver/Core/(AbstractCore
Core
Field handler namespace refactoring
`src/Drupal/Driver/Core/Field/(*Handler
FieldHandlerInterface).php`
Field handler lookup logic
src/Drupal/Driver/Core/AbstractCore.php
Rewrote getFieldHandler() to implement versioned lookup chain: tries Core{N}/Field/{Type}Handler for each version N down to 10, falls back through Core/Field/{Type}Handler and DefaultHandler, throwing RuntimeException if no handler found. Added protected getVersion(): int method (default returns 0).
Driver version resolution
src/Drupal/Driver/DrupalDriver.php
Modified setCoreFromVersion() to use lookup-chain instantiation: builds candidate class names Drupal\Driver\Core{N}\Core for detected version down to 10, falling back to Drupal\Driver\Core\Core. Now parses actual major version from Drupal VERSION string instead of hard-coding 8. Updated type imports for core-related interfaces.
Core test updates
`tests/Drupal/Tests/Driver/Core/(CoreFieldMethodsTest
CorePermissionsTest
Field handler test updates
tests/Drupal/Tests/Driver/Core/Field/(*HandlerTest).php
Updated test namespaces from Drupal\Tests\Driver\Fields\Drupal8 to Drupal\Tests\Driver\Core\Field and handler imports accordingly across ~15 test files. Updated PHPDoc return types for handler instantiation methods.
Core lookup and handler resolution tests
`tests/Drupal/Tests/Driver/(CoreLookupTest
Core/FieldHandlerLookupTest).php`
Test fixtures for versioned overrides
`tests/fixtures/Drupal/Driver/Core99/(Core
Field/FileHandler).php`
Configuration and documentation
composer.json, phpcs.xml, rector.php, `doc/(index
extending).rst, CONTRIBUTING.md, doc/_static/snippets/usage-drupal.php`
Spec test update
spec/Drupal/Driver/Core/CoreSpec.php
Renamed PhpSpec class from Drupal8Spec to CoreSpec, updated namespace from spec\Drupal\Driver\Cores to spec\Drupal\Driver\Core, and adjusted type references to new interface locations.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DrupalDriver
    participant CoreResolver
    participant Core99
    participant Core
    
    Client->>DrupalDriver: setCoreFromVersion(99)
    DrupalDriver->>CoreResolver: Build lookup chain for v99→10
    CoreResolver->>CoreResolver: Generate candidates: Core99, Core98...Core10, Core
    
    loop Try each candidate
        CoreResolver->>CoreResolver: class_exists(Drupal\Driver\Core99\Core)?
        alt Class exists
            CoreResolver->>Core99: Instantiate Core99
            Core99-->>DrupalDriver: Core99 instance (✓)
        else Try next version
            CoreResolver->>CoreResolver: class_exists(Core98)?
        end
    end
Loading
sequenceDiagram
    participant Test
    participant AbstractCore
    participant FieldResolver
    participant Core99Handler
    participant DefaultHandler
    
    Test->>AbstractCore: getFieldHandler(field_file, entity_type)
    AbstractCore->>FieldResolver: Build lookup chain for v99→10
    FieldResolver->>FieldResolver: Try Core99\Field\FileHandler
    
    alt v99 override exists
        FieldResolver->>Core99Handler: Instantiate Core99\Field\FileHandler
        Core99Handler-->>Test: Handler instance (Core99Handler with MARKER)
    else v99 doesn't exist
        FieldResolver->>FieldResolver: Try Core98...Core10
    else Fallback to default
        FieldResolver->>DefaultHandler: Instantiate Core\Field\FileHandler
        DefaultHandler-->>Test: Handler instance (Default)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • Epic: DrupalDriver 3.x #312: Directly addresses the same refactoring objective of moving from Cores/Drupal8 namespace to version-agnostic Core with per-version overrides and implementing the versioned handler lookup chain logic.

Possibly related PRs

Poem

🐰 From Drupal8 to Core we hop,
Version chains resolve, never stop—
Handlers lookup left and right,
Overrides picked, fallbacks tight.
Multi-version magic, oh what a sight! 🌟

🚥 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 change: a restructuring of core driver files into a 'Core/' directory with a version-override lookup chain mechanism.
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/restructure

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.46)

PHPStan was skipped because the config uses disallowed bootstrapFiles, bootstrapFile, or includes directives.


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 17, 2026

Code coverage (threshold: 35%)



Code Coverage Report Summary:
  Classes: 54.17% (13/24)
  Methods: 32.22% (58/180)
  Lines:   43.97% (332/755)

Per-class coverage
Drupal\Driver\BaseDriver                                      96.77%
Drupal\Driver\BlackboxDriver                                 100.00%
Drupal\Driver\Core\AbstractCore                               67.86%
Drupal\Driver\Core\Core                                       11.02%
Drupal\Driver\Core\Field\AbstractHandler                      81.82%
Drupal\Driver\Core\Field\AddressHandler                      100.00%
Drupal\Driver\Core\Field\DaterangeHandler                    100.00%
Drupal\Driver\Core\Field\DatetimeHandler                      46.67%
Drupal\Driver\Core\Field\DefaultHandler                      100.00%
Drupal\Driver\Core\Field\EmbridgeAssetItemHandler              0.00%
Drupal\Driver\Core\Field\EntityReferenceHandler               79.31%
Drupal\Driver\Core\Field\FileHandler                          94.44%
Drupal\Driver\Core\Field\ImageHandler                         92.86%
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                95.83%
Drupal\Driver\Core\Field\TaxonomyTermReferenceHandler        100.00%
Drupal\Driver\Core\Field\TextWithSummaryHandler              100.00%
Drupal\Driver\Core\Field\TimeHandler                         100.00%
Drupal\Driver\DrupalDriver                                    14.29%
Drupal\Driver\DrushDriver                                     22.76%
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: 3

🧹 Nitpick comments (10)
phpcs.xml (1)

19-19: Pattern won't cover version-override core directories.

src/Drupal/Driver/Core/*.php matches only files directly under Core/ (e.g., Core.php, AbstractCore.php). Per-version override directories introduced by this PR's pattern (Core12/, etc.) won't be covered if/when they also need to set $_SERVER during bootstrap, and will trip Drupal.Semantics.RemoteAddress.RemoteAddress. No overrides ship here so it's not blocking, but consider broadening now to avoid surprises later.

♻️ Suggested broadening
-        <exclude-pattern>src/Drupal/Driver/Core/*.php</exclude-pattern>
+        <exclude-pattern>src/Drupal/Driver/Core*/*.php</exclude-pattern>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@phpcs.xml` at line 19, The current phpcs exclude pattern
"src/Drupal/Driver/Core/*.php" only matches files directly under Core/ and will
miss per-version override directories like Core12/, so update the
exclude-pattern in phpcs.xml to broadly match Core and any versioned Core
directories (e.g., use a glob that covers Core*/** or Core*/**/*.php) so files
under Core12/, Core13/, etc. are also excluded from
Drupal.Semantics.RemoteAddress.RemoteAddress checks.
src/Drupal/Driver/Core/Field/TimeHandler.php (1)

5-9: Namespace migration looks correct.

Optional nit: the class docblock still says "Time field handler for Drupal 8" (line 8), which is now misleading given the handler lives under the version-agnostic Core\Field namespace and is the default across D10/11/12. Same applies to NameHandler, LinkHandler, and AddressHandler. Consider a sweep to drop the "for Drupal 8" phrasing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Drupal/Driver/Core/Field/TimeHandler.php` around lines 5 - 9, Update the
docblock for the TimeHandler class (and similarly for NameHandler, LinkHandler,
AddressHandler) to remove the phrase "for Drupal 8" and replace it with a
version-agnostic description such as "Time field handler" (or "Field handler for
core" / "Default time field handler") so the comment accurately reflects that
these handlers live under Core\Field and apply to D10/11/12; edit the docblock
above the TimeHandler class declaration to change the string and keep the
existing short description style.
doc/extending.rst (1)

70-70: Nit: double space.

"``Core`` and ``Field\*Handler``" contains a stray double space between Core and and.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/extending.rst` at line 70, Fix the stray double space in the
documentation line by replacing the two spaces between the symbols so it reads
"**Default implementation:** ``Core`` and ``Field*Handler``"; update the text
that references the Core and Field*Handler symbols to use a single space between
``Core`` and "and" to remove the double-space typo.
src/Drupal/Driver/Core/AbstractCore.php (2)

51-63: Confirm the 0 sentinel contract.

Returning 0 here plus the $n >= 10 loop guard in getFieldHandler() is an intentional "skip the versioned walk for the default Core" trick. Please add a matching note in CoreInterface (or wherever the contract is surfaced) so that anyone adding a new Core{N}\Core remembers to override getVersion() — otherwise their versioned field-handler directory will never be consulted, silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Drupal/Driver/Core/AbstractCore.php` around lines 51 - 63, Add a
documentation note to the CoreInterface explaining the sentinel contract: the
default AbstractCore::getVersion() returns 0 to skip versioned handler lookup
(the getFieldHandler() loop only iterates when $n >= 10), so any versioned core
classes (e.g., Core{N}\Core implementations) must override getVersion() to
return their major version number; update CoreInterface's docblock to describe
this behavior and the requirement to override getVersion() to ensure versioned
field-handler directories are considered.

68-91: Lookup chain is correct; minor dedup/perf opportunity.

The two for loops building $candidates and $default_candidates are structurally identical. You can flatten into a single interleaved list (per-version type handler → per-version default) or two small helpers, which also removes the array_merge call:

♻️ Example simplification
-    $candidates = [];
-    for ($n = $version; $n >= 10; $n--) {
-      $candidates[] = sprintf('\\Drupal\\Driver\\Core%d\\Field\\%sHandler', $n, $camelized_type);
-    }
-    $candidates[] = sprintf('\\Drupal\\Driver\\Core\\Field\\%sHandler', $camelized_type);
-
-    $default_candidates = [];
-    for ($n = $version; $n >= 10; $n--) {
-      $default_candidates[] = sprintf('\\Drupal\\Driver\\Core%d\\Field\\DefaultHandler', $n);
-    }
-    $default_candidates[] = DefaultHandler::class;
-
-    foreach (array_merge($candidates, $default_candidates) as $class) {
+    $candidates = [];
+    for ($n = $version; $n >= 10; $n--) {
+      $candidates[] = sprintf('\\Drupal\\Driver\\Core%d\\Field\\%sHandler', $n, $camelized_type);
+    }
+    $candidates[] = sprintf('\\Drupal\\Driver\\Core\\Field\\%sHandler', $camelized_type);
+    for ($n = $version; $n >= 10; $n--) {
+      $candidates[] = sprintf('\\Drupal\\Driver\\Core%d\\Field\\DefaultHandler', $n);
+    }
+    $candidates[] = DefaultHandler::class;
+
+    foreach ($candidates as $class) {
       if (class_exists($class)) {
         return new $class($entity, $entity_type, $field_name);
       }
     }
     throw new \RuntimeException(sprintf('No field handler found for type "%s".', $camelized_type));

Secondary concern: getFieldHandler() is on the hot path of expandEntityFields(), which calls it per field. Every miss triggers an autoloader lookup. In practice the range is 10–12 so this is negligible today, but if a per-(version,type) cache is cheap to add you’ll insulate yourself from future growth.

Also: the final throw is effectively unreachable because DefaultHandler::class is imported at the top and thus always resolvable — happy to keep it as defensive code, just noting it won’t be covered by any realistic test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Drupal/Driver/Core/AbstractCore.php` around lines 68 - 91,
getFieldHandler builds two identical per-version loops ($candidates and
$default_candidates) causing redundant work and repeated autoloader lookups;
refactor by constructing a single interleaved candidate list (for each version
push the type handler then the version DefaultHandler, then fall back to global
DefaultHandler::class) or extract a small helper to produce candidates so you
can remove array_merge, and add a simple static or private cache inside
getFieldHandler keyed by version+camelized type to remember the resolved handler
class (store the class name, not an instance) so subsequent calls do a direct
instantiation instead of re-trying class_exists for each candidate; keep the
final RuntimeException as defensive code and continue to instantiate the
resolved class with new $class($entity, $entity_type, $field_name).
tests/Drupal/Tests/Driver/CoreLookupTest.php (2)

41-47: Fallback test depends on Drupal\Driver\Core50\Core never existing.

testLookupFallsBackToDefault() uses 50 as the "no override" sentinel. This is fine today, but an innocuous future fixture named Core50 would silently break the test. Using an extreme/obviously-fake version like 999 (paired with a comment) or introducing a constant next to the fixture directory would remove that foot-gun.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/CoreLookupTest.php` around lines 41 - 47, The test
testLookupFallsBackToDefault() relies on createDriverWithVersion(50) assuming no
Core50 fixture exists; change the sentinel to an obviously fake/extreme version
(e.g. 999) or introduce a named constant (e.g. NO_OVERRIDE_VERSION) near the
test/fixture configuration and use that constant in createDriverWithVersion(),
then run setCoreFromVersion() and assertInstanceOf(Core::class,
$driver->getCore()) as before so the test no longer depends on the accidental
absence of a real Core50 fixture.

61-75: Reflection on readonly props works here, but fragile if DrupalDriver is ever reshuffled.

newInstanceWithoutConstructor() leaves drupalRoot/uri uninitialized, so ReflectionProperty::setValue() is permitted on the readonly declarations — that’s the only reason this works. If someone later moves either property out of DrupalDriver (or gives it a default), the reflection path will start throwing Error: Cannot modify readonly property.

Consider either:

  • A small factory hook in DrupalDriver (e.g., static createForTesting(int $version, string $root): self) that the test can call instead of poking private state, or
  • A brief comment on the helper documenting the newInstanceWithoutConstructor() + readonly dependency so future maintainers understand why this is written the way it is.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/CoreLookupTest.php` around lines 61 - 75, The test
helper createDriverWithVersion relies on
reflection/newInstanceWithoutConstructor to set readonly properties (drupalRoot
and uri) which is fragile; add a small public static factory on DrupalDriver
(e.g., DrupalDriver::createForTesting(int $version, string $drupalRoot, string
$uri = 'default')) that constructs a properly initialised instance for tests and
update createDriverWithVersion to call that factory instead of using
ReflectionProperty::setValue; alternatively, if you prefer not to change
production code, add a clear comment inside createDriverWithVersion explaining
the reliance on newInstanceWithoutConstructor(), ReflectionProperty::setValue(),
and readonly behavior so future maintainers understand why reflection is used.
tests/Drupal/Tests/Driver/FieldHandlerLookupTest.php (2)

118-122: ->with('bundle') makes the mock brittle.

$entity_type->method('getKey')->with('bundle')->willReturn('type') is scoped to a specific argument. If any current or future field handler calls getKey() with anything other than 'bundle' (e.g., 'id', 'label'), PHPUnit will fail the expectation rather than returning a default. Given the test only needs the field-lookup chain to resolve, consider removing the ->with(...) constraint, or using a willReturnMap() for the key names you know about.

♻️ Suggested loosening
-    $entity_type->method('getKey')->with('bundle')->willReturn('type');
+    $entity_type->method('getKey')->willReturnMap([
+      ['bundle', 'type'],
+    ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/FieldHandlerLookupTest.php` around lines 118 - 122,
The mock on EntityTypeInterface is too strict because
$entity_type->method('getKey')->with('bundle')->willReturn('type') will fail if
getKey() is called with any other argument; update the test by removing the
->with('bundle') constraint on the getKey() mock (or replace it with a
willReturnMap for the known key names) so getKey() returns expected values for
multiple keys; adjust the mock setup around $entity_type
(EntityTypeInterface::class, getKey) and ensure
$entity_type_manager->method('getDefinition') still returns $entity_type.

132-157: Consider moving Core99TestCore out of the test file.

Declaring a second top-level class in a PHPUnit test file works but has two downsides:

  • Test discovery tools occasionally flag multiple top-level classes per file.
  • The class can’t be reused by sibling tests (e.g., a future FieldHandlerLookupExtendedTest) without copy/paste.

Either move it to tests/fixtures/Drupal/Driver/Core99/TestCore.php (alongside the other Core99 fixtures — nicely symmetrical with the PR’s autoload-dev setup), or make it a private nested anonymous class if each test only needs a one-shot instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Drupal/Tests/Driver/FieldHandlerLookupTest.php` around lines 132 - 157,
Extract the Core99TestCore test helper (subclassing AbstractCore and
implementing getVersion() and getEntityFieldTypes()) out of this test file into
its own dedicated fixture class file so it can be reused by other tests and
avoids multiple top-level classes in one PHPUnit file; update the test to
instantiate that fixture instead of declaring the class inline, or alternatively
replace the inline class with a private nested anonymous class if the helper is
truly one-off.
src/Drupal/Driver/DrupalDriver.php (1)

227-241: Lookup chain is correct; one edge-case & minor perf note.

The descending $version..10 walk plus Core::class terminal is fine and matches AbstractCore::getFieldHandler()'s pattern for consistency.

Two small observations:

  1. If $version < 10 (shouldn't happen today because detectMajorVersion() already guards), the for loop is simply skipped and only Core::class is tried. That's benign, but worth an assertion or early-return comment to make the invariant explicit for future maintainers.
  2. class_exists() triggers autoload on every candidate. For a large detected version (e.g., the 99 used by lookup tests) this amounts to ~90 failed autoload attempts on a cold run. In production where the range is 10–12 this is trivially 1–3 attempts, so not a blocker — just flagging for future-proofing if the ceiling ever grows.

Note: the CI ParseError reported on this line is a downstream effect of a PHP 8.3 typed-constant in the Core99 fixture — see the comment on tests/Drupal/Tests/Driver/CoreLookupTest.php for the root cause and fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Drupal/Driver/DrupalDriver.php` around lines 227 - 241, Add an explicit
invariant check and a short note about autoload cost in setCoreFromVersion():
assert or throw if $this->getDrupalVersion() < 10 to make the precondition
explicit (so callers/maintainers know detectMajorVersion() must have run), and
add a brief comment above the for-loop documenting that class_exists() triggers
autoload attempts and may be costly for large ranges (hinting that future
optimization could use class_exists($class, false) or a bounded autoload
strategy); keep the existing candidate lookup and BootstrapException behavior
unchanged and reference the setCoreFromVersion(), getDrupalVersion(),
Core::class and $this->core symbols so the change is easy to find.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/extending.rst`:
- Around line 115-122: The example for Core class lookup misrepresents the
short-circuit behavior of setCoreFromVersion(): after finding Core13\Core it
returns and does not evaluate later fallbacks, so change the illustration to
either stop the chain at "Try: Drupal\Driver\Core13\Core ← exists, use it" or
relabel subsequent lines (e.g. "Try: Drupal\Driver\Core12\Core ← not reached",
"Try: Drupal\Driver\Core\Core ← not reached") to accurately reflect that
setCoreFromVersion() returns on the first match.

In `@tests/Drupal/Tests/Driver/CoreLookupTest.php`:
- Around line 30-36: Remove the PHP 8.3 typed class-constant annotations from
the test fixtures so they parse on PHP 8.2: update the Core99\Core class
constant (currently declared as "public const string MARKER = 'Core99\\Core';")
to an untyped constant ("public const MARKER = 'Core99\\Core';") and do the same
for the constant(s) in Core99\Field\FileHandler; do not change composer.json
(leave PHP ^8.2) unless you intentionally want to adopt Option B and raise the
minimum PHP to 8.3.

In `@tests/fixtures/Drupal/Driver/Core99/Core.php`:
- Line 21: The class constant declaration "public const string MARKER" uses a
PHP 8.3 typed-constant feature and breaks parsing on PHP 8.2; remove the type
annotation so the constant is declared as "public const MARKER" (retain the same
value 'Core99\\Core') within the Core class (symbol: MARKER) to restore
compatibility with PHP 8.2.

---

Nitpick comments:
In `@doc/extending.rst`:
- Line 70: Fix the stray double space in the documentation line by replacing the
two spaces between the symbols so it reads "**Default implementation:** ``Core``
and ``Field*Handler``"; update the text that references the Core and
Field*Handler symbols to use a single space between ``Core`` and "and" to remove
the double-space typo.

In `@phpcs.xml`:
- Line 19: The current phpcs exclude pattern "src/Drupal/Driver/Core/*.php" only
matches files directly under Core/ and will miss per-version override
directories like Core12/, so update the exclude-pattern in phpcs.xml to broadly
match Core and any versioned Core directories (e.g., use a glob that covers
Core*/** or Core*/**/*.php) so files under Core12/, Core13/, etc. are also
excluded from Drupal.Semantics.RemoteAddress.RemoteAddress checks.

In `@src/Drupal/Driver/Core/AbstractCore.php`:
- Around line 51-63: Add a documentation note to the CoreInterface explaining
the sentinel contract: the default AbstractCore::getVersion() returns 0 to skip
versioned handler lookup (the getFieldHandler() loop only iterates when $n >=
10), so any versioned core classes (e.g., Core{N}\Core implementations) must
override getVersion() to return their major version number; update
CoreInterface's docblock to describe this behavior and the requirement to
override getVersion() to ensure versioned field-handler directories are
considered.
- Around line 68-91: getFieldHandler builds two identical per-version loops
($candidates and $default_candidates) causing redundant work and repeated
autoloader lookups; refactor by constructing a single interleaved candidate list
(for each version push the type handler then the version DefaultHandler, then
fall back to global DefaultHandler::class) or extract a small helper to produce
candidates so you can remove array_merge, and add a simple static or private
cache inside getFieldHandler keyed by version+camelized type to remember the
resolved handler class (store the class name, not an instance) so subsequent
calls do a direct instantiation instead of re-trying class_exists for each
candidate; keep the final RuntimeException as defensive code and continue to
instantiate the resolved class with new $class($entity, $entity_type,
$field_name).

In `@src/Drupal/Driver/Core/Field/TimeHandler.php`:
- Around line 5-9: Update the docblock for the TimeHandler class (and similarly
for NameHandler, LinkHandler, AddressHandler) to remove the phrase "for Drupal
8" and replace it with a version-agnostic description such as "Time field
handler" (or "Field handler for core" / "Default time field handler") so the
comment accurately reflects that these handlers live under Core\Field and apply
to D10/11/12; edit the docblock above the TimeHandler class declaration to
change the string and keep the existing short description style.

In `@src/Drupal/Driver/DrupalDriver.php`:
- Around line 227-241: Add an explicit invariant check and a short note about
autoload cost in setCoreFromVersion(): assert or throw if
$this->getDrupalVersion() < 10 to make the precondition explicit (so
callers/maintainers know detectMajorVersion() must have run), and add a brief
comment above the for-loop documenting that class_exists() triggers autoload
attempts and may be costly for large ranges (hinting that future optimization
could use class_exists($class, false) or a bounded autoload strategy); keep the
existing candidate lookup and BootstrapException behavior unchanged and
reference the setCoreFromVersion(), getDrupalVersion(), Core::class and
$this->core symbols so the change is easy to find.

In `@tests/Drupal/Tests/Driver/CoreLookupTest.php`:
- Around line 41-47: The test testLookupFallsBackToDefault() relies on
createDriverWithVersion(50) assuming no Core50 fixture exists; change the
sentinel to an obviously fake/extreme version (e.g. 999) or introduce a named
constant (e.g. NO_OVERRIDE_VERSION) near the test/fixture configuration and use
that constant in createDriverWithVersion(), then run setCoreFromVersion() and
assertInstanceOf(Core::class, $driver->getCore()) as before so the test no
longer depends on the accidental absence of a real Core50 fixture.
- Around line 61-75: The test helper createDriverWithVersion relies on
reflection/newInstanceWithoutConstructor to set readonly properties (drupalRoot
and uri) which is fragile; add a small public static factory on DrupalDriver
(e.g., DrupalDriver::createForTesting(int $version, string $drupalRoot, string
$uri = 'default')) that constructs a properly initialised instance for tests and
update createDriverWithVersion to call that factory instead of using
ReflectionProperty::setValue; alternatively, if you prefer not to change
production code, add a clear comment inside createDriverWithVersion explaining
the reliance on newInstanceWithoutConstructor(), ReflectionProperty::setValue(),
and readonly behavior so future maintainers understand why reflection is used.

In `@tests/Drupal/Tests/Driver/FieldHandlerLookupTest.php`:
- Around line 118-122: The mock on EntityTypeInterface is too strict because
$entity_type->method('getKey')->with('bundle')->willReturn('type') will fail if
getKey() is called with any other argument; update the test by removing the
->with('bundle') constraint on the getKey() mock (or replace it with a
willReturnMap for the known key names) so getKey() returns expected values for
multiple keys; adjust the mock setup around $entity_type
(EntityTypeInterface::class, getKey) and ensure
$entity_type_manager->method('getDefinition') still returns $entity_type.
- Around line 132-157: Extract the Core99TestCore test helper (subclassing
AbstractCore and implementing getVersion() and getEntityFieldTypes()) out of
this test file into its own dedicated fixture class file so it can be reused by
other tests and avoids multiple top-level classes in one PHPUnit file; update
the test to instantiate that fixture instead of declaring the class inline, or
alternatively replace the inline class with a private nested anonymous class if
the helper is truly one-off.
🪄 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: b57fe4ca-e8f1-4fcb-b5c3-6e6df321a590

📥 Commits

Reviewing files that changed from the base of the PR and between ce6713b and b6e6585.

📒 Files selected for processing (53)
  • composer.json
  • doc/extending.rst
  • doc/index.rst
  • phpcs.xml
  • rector.php
  • spec/Drupal/Driver/Core/CoreSpec.php
  • src/Drupal/Driver/Core/AbstractCore.php
  • src/Drupal/Driver/Core/Core.php
  • src/Drupal/Driver/Core/CoreAuthenticationInterface.php
  • src/Drupal/Driver/Core/CoreInterface.php
  • src/Drupal/Driver/Core/Field/AbstractHandler.php
  • src/Drupal/Driver/Core/Field/AddressHandler.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/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/ListIntegerHandler.php
  • src/Drupal/Driver/Core/Field/ListStringHandler.php
  • src/Drupal/Driver/Core/Field/NameHandler.php
  • src/Drupal/Driver/Core/Field/OgStandardReferenceHandler.php
  • src/Drupal/Driver/Core/Field/SupportedImageHandler.php
  • src/Drupal/Driver/Core/Field/TaxonomyTermReferenceHandler.php
  • src/Drupal/Driver/Core/Field/TextWithSummaryHandler.php
  • src/Drupal/Driver/Core/Field/TimeHandler.php
  • src/Drupal/Driver/DrupalDriver.php
  • tests/Drupal/Tests/Driver/Core/Field/AddressHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/DaterangeHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/DatetimeHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/DefaultHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/EntityReferenceHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/FileHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/ImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/ListHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/SupportedImageHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/TaxonomyTermReferenceHandlerTest.php
  • tests/Drupal/Tests/Driver/Core/Field/TextWithSummaryHandlerTest.php
  • tests/Drupal/Tests/Driver/CoreLookupTest.php
  • tests/Drupal/Tests/Driver/Drupal8FieldMethodsTest.php
  • tests/Drupal/Tests/Driver/Drupal8PermissionsTest.php
  • tests/Drupal/Tests/Driver/Drupal8Test.php
  • tests/Drupal/Tests/Driver/FieldHandlerLookupTest.php
  • tests/Drupal/Tests/Driver/LinkHandlerTest.php
  • tests/Drupal/Tests/Driver/NameHandlerTest.php
  • tests/Drupal/Tests/Driver/TimeHandlerTest.php
  • tests/fixtures/Drupal/Driver/Core99/Core.php
  • tests/fixtures/Drupal/Driver/Core99/Field/FileHandler.php

Comment thread doc/extending.rst
Comment thread tests/Drupal/Tests/Driver/CoreLookupTest.php
Comment thread tests/fixtures/Drupal/Driver/Core99/Core.php Outdated
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