Skip to content

v3.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 29 May 01:28
9b83f4a

What's new since v3.0.0-rc2

  • Fixed documentation inaccuracies surfaced during v3.0.0 release review. @AlexSkrypnyk (#373)

Full Changelog: v3.0.0-rc2...v3.0.0

@AlexSkrypnyk

Upgrading from v2

See UPGRADING.md for the full v2 to v3 migration guide.

Implemented across pre-stable v3 releases

This release rolls up everything shipped in the v3.0.0 prerelease cycle (v3.0.0-alpha1v3.0.0-rc1v3.0.0-rc2). Each section below preserves the original release notes for that prerelease.


v3.0.0-alpha1 (May 3, 2026)

v3 reworks the driver contract into a set of composable capability interfaces, narrows entity stubs to a typed value object, drops Drupal 6/7, and tightens the supported PHP/Drupal range. Most changes are mechanical; a small amount of consumer-side code (typically in DrupalExtension integrations) may need updating.

Highlights

  • [v3] Restructured driver and core contracts into composable capability interfaces. (#342)
    The driver surface is now a set of twelve small, named capability interfaces (AuthenticationCapabilityInterface, CacheCapabilityInterface, ContentCapabilityInterface, ...) composed into per-driver contracts. Consumers can instanceof-check exactly the capability they need (if ($driver instanceof MailCapabilityInterface) { ... }) instead of catching UnsupportedDriverActionException from a giant base class, and downstream projects can build new drivers by implementing only the capabilities they actually support.
  • [#155] Added field handler registry with directory-based discovery and CoreInterface injection. (#353)
    Consumer projects now have two real extension seams that don't require squatting on this project's namespace: $core->registerFieldHandler('my_type', MyHandler::class) overrides one handler from anywhere, and $driver->setCore($custom_core) swaps the entire Core for a consumer-owned subclass. Internally the project dogfoods the same mechanism via a glob-based scan of the sibling Field/ directory, so future Drupal versions add handlers by file-drop instead of editing a hardcoded list.
  • [#111] Added 'BlockCapabilityInterface' with block placement and content-block methods. (#354)
    Closes a 2016 gap (#111): blockPlace(), blockDelete(), blockContentCreate(), and blockContentDelete() are now first-class driver operations on D10/D11, supporting both the block config entity (placement in a region) and the block_content content entity (custom block bodies). Block-type creation is intentionally out of scope - like node types and vocabularies, that is schema-level setup that belongs in the test site's configuration, not in a scenario step.
  • Added 'FieldClassifier' with nine F-row predicates and rewrote field-expansion pipeline. (#359)
    The new FieldClassifier provides nine origin/storage predicates (F1-F9) that classify every Drupal field, and Core::expandEntityFields() now routes through them so only writable, handler-eligible fields enter the pipeline. Previously, computed and non-writable base fields slipped through the configured-field filter and produced silent garbage; now DefaultHandler throws loudly for any multi-column type without a registered handler so gaps surface immediately. A new EntityReferenceRevisionsHandler covers paragraph and other revision-tracked references.
  • [#362] Replaced 'stdClass' entity stubs with typed 'EntityStub' class. (#363)
    The untyped \stdClass shape that flowed between callers and every driver create/delete method was replaced with a typed EntityStub carrying entity type, bundle, a mutable values bag, and a saved-entity slot. Callers can now use typed accessors ($stub->getId(), $stub->isSaved(), $stub->getSavedEntity()) instead of guessing which property holds the saved id, and the field-handler boundary is also typed so the impedance mismatch between the driver and the expand pipeline is gone.
  • [#124] Added 'BooleanHandler' accepting field labels and canonical yes/no/true/false/on/off forms. (#350)
    Scenarios stubbing boolean fields no longer have to use raw 1/0. The handler matches the field's own on_label / off_label first (case-insensitive, so a German site with Ja/Nein works verbatim), then falls back to the canonical English allow-list via filter_var(FILTER_VALIDATE_BOOLEAN). Anything outside both sets throws a RuntimeException listing the accepted values, replacing silent coercion to FALSE.
  • [#118] Improved error messages for unknown entity types, bundles, vocabularies, and parent terms. (#351)
    Stubs referencing a missing entity type, bundle, vocabulary, or parent term now fail with an InvalidArgumentException that quotes the offending value (Unknown entity type "fake_type"., Cannot create term because parent term 'Missing' does not exist in vocabulary 'tags'.) instead of leaking low-level plugin-system errors or undefined-property warnings. Scenario authors get an actionable message at the failure boundary instead of a stack trace into Drupal internals.

Breaking changes

  • [#362] Replaced 'stdClass' entity stubs with typed 'EntityStub' class. (#363)
    Every capability interface that previously accepted or returned \stdClass (AuthenticationCapabilityInterface::login(), every Block*, Content*, Language*, and User* create/delete method) now declares EntityStubInterface. Migration: replace (object) ['type' => 'page', ...] constructs with new \Drupal\Driver\Entity\EntityStub('node', 'page', [...]), and read the saved id via $stub->getId() / saved entity via $stub->getSavedEntity() instead of guessing property names. There is no \stdClass shim - this is a clean cut.
  • Added 'FieldClassifier' with nine F-row predicates and rewrote field-expansion pipeline. (#359)
    FieldCapabilityInterface is removed along with fieldExists() and fieldIsBase() on Core and DrupalDriverInterface; both predicates now live on FieldClassifierInterface. Core::expandEntityFields() no longer accepts a $base_fields argument and Core::getEntityFieldTypes() now takes ?string $bundle instead of array $base_fields. Core::expandEntityBaseFields() and Core::detectBaseFieldsOnEntity() were removed. DefaultHandler::expand() now throws \RuntimeException for any field whose storage is not a single value column instead of silently emitting garbage. Migration is documented in UPGRADING.md.
  • Tightened 'DrupalDriverInterface' contract and 'DrupalDriver' visibility. (#358)
    getCore(), setCore(), and getDrupalVersion() are now part of the DrupalDriverInterface contract, so any consumer that implements the interface directly must add these three methods. DrupalDriver::$core and DrupalDriver::$version were narrowed from public to protected; replace direct property access ($driver->core, $driver->version) with the new public accessors.
  • [#155] Added field handler registry with directory-based discovery and CoreInterface injection. (#353)
    DrupalDriver::setCore() signature changed from setCore(array $available_cores) (keyed by Drupal version) to setCore(CoreInterface $core). Migration: callers that previously passed a version-keyed array of core classes must now construct or select a single CoreInterface instance themselves and inject it. The convention-based setCoreFromVersion() lookup that walks Drupal\Driver\Core{N}\Core classes is unchanged.
  • [#154] Removed unused 'TaxonomyTermReferenceHandler' and its tests. (#352)
    The handler matched the legacy taxonomy_term_reference field type that Drupal core removed in 8.0.0-beta10 (2015) and was unreachable on every supported Drupal version. Modern taxonomy-linking fields are entity_reference with target_type = taxonomy_term and route through EntityReferenceHandler; consumers that subclassed TaxonomyTermReferenceHandler should subclass EntityReferenceHandler instead.
  • [#270] Fixed 'entityCreate()' base field expansion and dynamic id key resolution. (#346)
    entityCreate() now populates the entity-type-specific id property (e.g. $stub->uid for users, $stub->nid for nodes) instead of always assigning to $stub->id, and entityDelete() reads from the same key. Consumers reading $stub->id after a generic entityCreate('user', $stub) must switch to $stub->$id_key (resolved via $entity_type_definition->getKey('id')) or read from the returned entity. Base fields set on the stub (e.g. commerce_product.variations) are now auto-routed through the handler pipeline rather than reaching storage in raw form.
  • [v3] Removed 'drush-ops/behat-drush-endpoint' and Drush-backed entity and field methods. (#343)
    DrushDriverInterface no longer extends ContentCapabilityInterface or FieldCapabilityInterface. entityCreate, entityDelete, nodeCreate, nodeDelete, termCreate, termDelete, fieldExists, and fieldIsBase are gone from DrushDriver, and the drush-ops/behat-drush-endpoint companion module is no longer required. Consumers that need entity CRUD or field introspection should switch to DrupalDriver; see UPGRADING.md for the per-method migration table.
  • [v3] Restructured driver and core contracts into composable capability interfaces. (#342)
    The monolithic DriverInterface / BaseDriver model was replaced with twelve fine-grained Capability\*CapabilityInterface interfaces and per-driver composite interfaces (DrupalDriverInterface, DrushDriverInterface, BlackboxDriverInterface). Many methods were renamed to lead with their capability prefix (createNodenodeCreate, createTermtermCreate, createEntityentityCreate, isFieldfieldExists, isBaseFieldfieldIsBase, clearCachecacheClear, clearStaticCachescacheClearStatic, runCroncronRun, fetchWatchdogwatchdogFetch, all *Mail methods → mail*). BaseDriver, AbstractCore, AuthenticationDriverInterface, CoreAuthenticationInterface, and DrupalCoreInterface were removed. UPGRADING.md documents every rename and the instanceof capability-check pattern.
  • Restructured cores into 'Core/' with version-override lookup chain. (#338)
    src/Drupal/Driver/Cores/Drupal8.php was renamed to src/Drupal/Driver/Core/Core.php (class Drupal8Core) and src/Drupal/Driver/Fields/Drupal8/* moved to src/Drupal/Driver/Core/Field/*. Any code referencing Drupal\Driver\Cores\Drupal8, Drupal\Driver\Fields\Drupal8\*, or the old Cores/ and Fields/ namespaces must update to Drupal\Driver\Core\Core and Drupal\Driver\Core\Field\*. A future Drupal version that diverges can ship a Drupal\Driver\Core{N}\Core subclass and the setCoreFromVersion() chain will pick it up automatically.
  • Added 'declare(strict_types=1)' and PHP 8.2 type declarations via Rector. (#337)
    Every source and test file now declares strict types and parameters/returns/properties carry PHP 8.2 type declarations. Callers that previously relied on PHP's loose-mode coercion (passing strings where ints are expected, etc.) will now hit TypeError at the boundary; audit any consumer code that calls into Core, DrupalDriver, DrushDriver, or any handler.
  • Removed Drupal 6 and 7 support and bumped composer constraints for 3.x. (#336)
    Minimum PHP is now 8.2 and supported Drupal versions are 10 and 11 only; D6 and D7 core classes, field handlers, and tests were deleted. DrupalDriver::detectMajorVersion() throws a BootstrapException when it sees Drupal < 10. Symfony deps were lifted to ^6.4 || ^7. Sites still on PHP 8.1 or Drupal 9 must stay on the 2.x line (now maintained on the 2.x branch).

All changes

  • [#362] Replaced 'stdClass' entity stubs with typed 'EntityStub' class. (#363)
    Every capability interface that touched \stdClass now declares EntityStubInterface, the field-handler boundary accepts the typed stub directly, and the previous (object) [...] constructs are replaced with new EntityStub($entity_type, $bundle, [...]). Callers can read the saved id and entity through typed accessors instead of guessing property names.
  • Added 'DrupalExtension' downstream smoke check job to CI.
    A new CI job runs the drupalextension test suite against this branch so regressions that only surface in the primary downstream consumer are caught before release.
  • Restored 'text_long', 'text', 'file', 'image' field-handler semantics and added type-coverage safety net. (#360)
    The v3 pipeline rewrite shipped without TextHandler and TextLongHandler, both multi-column types that immediately tripped the new loud-failure policy in DefaultHandler. This adds both handlers, restores the 2.x reuse-by-URI / reuse-by-basename behaviour for FileHandler (and ImageHandler, which now extends FileHandler), and adds FieldTypeCoverageKernelTest that walks every registered field type and fails if a new core type is missing a handler entry.
  • Added 'FieldClassifier' with nine F-row predicates and rewrote field-expansion pipeline. (#359)
    FieldClassifier exposes nine predicates that classify each field by origin (base / configurable / bundle-storage-backed) and storage profile (standard / computed / custom). Core::expandEntityFields() now routes through the classifier so only F1, F5, and F9 rows enter the handler pipeline, computed and non-writable fields no longer reach storage, and DefaultHandler throws for any multi-column type without a registered handler. EntityReferenceRevisionsHandler is added for paragraphs and other revision-tracked references; FieldCapabilityInterface and the fieldExists / fieldIsBase predicates are removed.
  • Tightened 'DrupalDriverInterface' contract and 'DrupalDriver' visibility. (#358)
    getCore(), setCore(), and getDrupalVersion() are now declared on DrupalDriverInterface, and $core / $version on DrupalDriver were narrowed from public to protected. Consumers that hand-rolled a DrupalDriverInterface implementation must add the three accessors, and any callers reading the properties directly must switch to the public methods.
  • [#111] Added 'BlockCapabilityInterface' with block placement and content-block methods. (#354)
    blockPlace(), blockDelete(), blockContentCreate(), and blockContentDelete() are now first-class capability methods on DrupalDriver and Core. Placement covers the block config entity (id auto-generated when omitted, plugin/theme/region wiring), and the content-block methods cover the block_content content entity via the generic field-expansion pipeline. Block-type creation is intentionally out of scope.
  • [#155] Added field handler registry with directory-based discovery and CoreInterface injection. (#353)
    Consumers register custom handlers with $core->registerFieldHandler($field_type, $class) (last registration wins), and $driver->setCore(CoreInterface $core) lets them inject any CoreInterface implementation. Internally Core::__construct() calls registerDefaultFieldHandlers() which scans the sibling Field/ directory and registers every concrete *Handler class, so future Drupal versions add handlers additively by dropping a file into a Core{N}\Field\ directory.
  • [#154] Removed unused 'TaxonomyTermReferenceHandler' and its tests. (#352)
    The handler matched the legacy taxonomy_term_reference field type that Drupal core removed in 8.0.0-beta10 (2015) and was unreachable on every supported Drupal version. Modern taxonomy references go through EntityReferenceHandler (already covered by the existing kernel test), so the orphaned class and its two tests are gone.
  • [#118] Improved error messages for unknown entity types, bundles, vocabularies, and parent terms. (#351)
    entityCreate() and entityDelete() now throw InvalidArgumentException('Unknown entity type "X".') when given a bad type instead of leaking PluginNotFoundException. termCreate() validates the vocabulary_machine_name property, the existence of the named vocabulary, and the existence of any named parent term up front, with messages that quote the offending value.
  • [#124] Added 'BooleanHandler' accepting field labels and canonical yes/no/true/false/on/off forms. (#350)
    The new handler reads on_label / off_label from the field config and matches them case-insensitively, then falls back to filter_var(FILTER_VALIDATE_BOOLEAN) for the canonical English forms. Localised label sets (e.g. Ja/Nein) work for free; values that match neither set throw a RuntimeException listing all accepted forms.
  • [#202] Allowed explicit id and label on 'roleCreate()'. (#349)
    roleCreate(array $permissions, ?string $id = NULL, ?string $label = NULL) lets callers name roles explicitly while preserving the existing random-id behaviour when both arguments are omitted. Closes a 2018 issue: scenarios that assert against a role by name no longer have to scrape the generated id out of the return value. When only $id is given, it doubles as the human-readable label.
  • Decoupled dev dependencies from 'drupal/core-dev' metapackage. (#348)
    The drupal/core-dev requirement was replaced with the three transitive deps the test suite actually needs (vfsstream, prophecy-phpunit, phpstan/extension-installer), and a project-owned tests/bootstrap.php replaces the upstream Drupal bootstrap so the Mink class-alias line no longer fatals when Behat/Mink is absent. The local install footprint drops by roughly 50 transitive packages.
  • Fixed 'EntityReferenceHandler::expand()' to preserve extra item properties on associative-array deltas. (#347)
    Associative-array deltas (the shape file fields, image fields, and entity_reference_revisions use - ['target_id' => 'alice', 'display' => 1]) previously crashed with Calling Drupal\Core\Database\Query\Condition::condition() without an array compatible operator is not supported. The handler now extracts the field's main property as the lookup label, resolves it to an id, writes the id back into the array, and round-trips the remaining columns intact.
  • [#270] Fixed 'entityCreate()' base field expansion and dynamic id key resolution. (#346)
    expandEntityFields() now auto-detects base fields set as properties on the stub (excluding the id and bundle keys) and routes them through the handler pipeline, so base entity-reference fields like commerce_product.variations resolve their labels instead of reaching storage raw. entityCreate() and entityDelete() now resolve the id key per entity type (uid, nid, tid, etc.) instead of hardcoding id.
  • Refactored source files for readability. (#345)
    Vertical-rhythm pass over twelve source files in src/Drupal/Driver/: guard clauses replace nested conditionals, blank lines bracket if / foreach / for blocks, generic $return variables are renamed to semantic names, and step-by-step comments that just restated the code are removed. One incidental correctness fix in DrushDriver::drush() replaces an !== NULL check on a typed non-nullable property with isset() so root-mode no longer trips on uninitialised access.
  • [v3] Reordered driver methods into canonical capability-based layout and widened property visibility to 'protected'. (#344)
    Public methods on DrupalDriver and DrushDriver are now grouped alphabetically by capability (Authentication, Cache, Config, Content, ...) so navigating between the two drivers is consistent. $bootstrapped, $drupalRoot, $uri, $random, and $arguments were widened from private to protected so subclasses can access them. No logic, signatures, or behaviour changed.
  • [v3] Removed 'drush-ops/behat-drush-endpoint' and Drush-backed entity and field methods. (#343)
    DrushDriverInterface no longer extends ContentCapabilityInterface or FieldCapabilityInterface, and the eight methods that required the companion endpoint module (entityCreate, entityDelete, nodeCreate, nodeDelete, termCreate, termDelete, fieldExists, fieldIsBase) are removed from DrushDriver. Consumers that need those operations should use DrupalDriver; see UPGRADING.md.
  • [v3] Restructured driver and core contracts into composable capability interfaces. (#342)
    Twelve fine-grained Capability\*CapabilityInterface interfaces replace the monolithic DriverInterface / BaseDriver model; each driver opts into the capabilities it actually services. Many methods were renamed to lead with their capability (createNodenodeCreate, clearCachecacheClear, runCroncronRun, isFieldfieldExists, the *Mail family flipped to mail*); BaseDriver, AbstractCore, AuthenticationDriverInterface, CoreAuthenticationInterface, and DrupalCoreInterface are removed.
  • Added kernel test infrastructure for field handler round-trips. (#341)
    A shared FieldHandlerKernelTestBase with attachField() and assertFieldRoundTripViaDriver() helpers backs new kernel tests for fifteen field handlers (Datetime, Link, ListString, ListInteger, ListFloat, Daterange, TextWithSummary, Default, EntityReference, File, Image, Address, Name, SupportedImage, Time). Writing the tests exposed and fixed two latent bugs: Core::entityCreate() returned the wrong type, and Core::userCreate() re-validated the Drupal site even when bootstrap had already done so.
  • Removed Docker from CI in favour of 'shivammathur/setup-php' action. (#340)
    The CI workflow now runs PHP and Composer directly on the GitHub-hosted runner via shivammathur/setup-php@v2, with actions/cache for the Composer cache. The docker-compose.yml and the wodby/drupal-php image dependency are gone, removing the PHP_VERSION / DOCKER_USER_ID env scaffolding the container needed.
  • Removed 'doc/' in favour of using markdown in repo. (#339)
    The Sphinx tree (Makefile, conf.py, eight .rst files, snippets) and the stale ReadTheDocs link are removed; the architecture/extending guide that lived in doc/extending.rst is condensed into a new "Extending for a new Drupal version" section in CONTRIBUTING.md.
  • Restructured cores into 'Core/' with version-override lookup chain. (#338)
    src/Drupal/Driver/Cores/Drupal8.php becomes src/Drupal/Driver/Core/Core.php and the seventeen handlers under Fields/Drupal8/ move to Core/Field/. DrupalDriver::setCoreFromVersion() and Core::getFieldHandler() walk a Core{N}\ chain so a future Drupal version that diverges can ship an override file alongside the default without copy-pasting the whole implementation. No Core{N}/ directory exists today; everything resolves to the default Core\Core and Core\Field\* classes.
  • Added 'declare(strict_types=1)' and PHP 8.2 type declarations via Rector. (#337)
    Strict types and PHP 8.2 parameter / return / property declarations were applied across all 56 source and test files via a Rector config that mirrors the Drupal Extension setup (palantirnet/drupal-rector was dropped in favour of rector/rector ^2.0 directly). Constructor property promotion was reverted in three classes where it mangled existing docblocks, and that rule is now skipped going forward.
  • Removed Drupal 6 and 7 support and bumped composer constraints for 3.x. (#336)
    D6 and D7 cores, field handlers, and tests are deleted; PHP is now ^8.2, Drupal is ^10 || ^11, Symfony is ^6.4 || ^7. The CI matrix doubles to ten entries because each PHP/Drupal combination now runs under both --prefer-lowest and normal dependency resolution. master is the active 3.x branch; the maintenance line lives on the new 2.x branch.
  • Removed 'CHANGELOG.md' in favour of GitHub Releases. (#335)
    The 199-line in-repo changelog was redundant with the draft-release-notes.yml workflow that already publishes notes via GitHub Releases. The "Release notes" link in README.md now points at the Releases page.
  • Split 'lint' job out of 'tests' job in CI workflow. (#334)
    Lint and composer normalize --dry-run were moved into a dedicated job that runs once on PHP 8.4 / Drupal 11 instead of repeating across every matrix combination. The tests job retains its full PHP/Drupal matrix and coverage logic.

Full Changelog: v2.5.1...v3.0.0-alpha1


v3.0.0-rc1 (May 18, 2026)

Full Changelog: v3.0.0-alpha1...v3.0.0-rc1


v3.0.0-rc2 (May 19, 2026)

All changes

  • Added 'SmartdateHandler' for the smart_date contrib module. @AlexSkrypnyk (#371)
    Closes a v3.0 regression: writing to a smartdate field (six storage columns from the smart_date contrib module) threw RuntimeException because v3.0 hardened DefaultHandler to reject unknown multi-column fields. The new handler accepts a positional [start, end] pair, a keyed record, or a list of either form for multi-delta fields; parses numeric Unix timestamps and strtotime()-compatible strings; and auto-derives duration as (end - start) / 60 clamped to zero when both endpoints are supplied without an explicit duration.
  • Fixed 'ImageHandler' and 'DatetimeHandler' compound parser shape. @AlexSkrypnyk (#370)
    Fixes a TypeError in ImageHandler::expand() when callers handed it a list-of-records shape like [['target_id' => 'foo.jpg', ...]] (which the v6 DrupalExtension field parser naturally produces) instead of a bare path. Adds a shared normalise() helper on AbstractHandler that folds any loose caller shape - bare scalar, list of scalars, single record, list of records, or mixed list - into a canonical list of records, and migrates ImageHandler, DatetimeHandler, and the three text handlers onto it. The ambiguous "positional value + named extras" shape (e.g. ['/path', 'alt' => 'A']) is now rejected with InvalidArgumentException instead of being silently disambiguated.

Full Changelog: v3.0.0-rc1...v3.0.0-rc2