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
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-alpha1 → v3.0.0-rc1 → v3.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 caninstanceof-check exactly the capability they need (if ($driver instanceof MailCapabilityInterface) { ... }) instead of catchingUnsupportedDriverActionExceptionfrom 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 entireCorefor a consumer-owned subclass. Internally the project dogfoods the same mechanism via a glob-based scan of the siblingField/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(), andblockContentDelete()are now first-class driver operations on D10/D11, supporting both theblockconfig entity (placement in a region) and theblock_contentcontent 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 newFieldClassifierprovides nine origin/storage predicates (F1-F9) that classify every Drupal field, andCore::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; nowDefaultHandlerthrows loudly for any multi-column type without a registered handler so gaps surface immediately. A newEntityReferenceRevisionsHandlercovers paragraph and other revision-tracked references. - [#362] Replaced 'stdClass' entity stubs with typed 'EntityStub' class. (#363)
The untyped\stdClassshape that flowed between callers and every driver create/delete method was replaced with a typedEntityStubcarrying 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 stubbingbooleanfields no longer have to use raw1/0. The handler matches the field's ownon_label/off_labelfirst (case-insensitive, so a German site withJa/Neinworks verbatim), then falls back to the canonical English allow-list viafilter_var(FILTER_VALIDATE_BOOLEAN). Anything outside both sets throws aRuntimeExceptionlisting the accepted values, replacing silent coercion toFALSE. - [#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 anInvalidArgumentExceptionthat 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(), everyBlock*,Content*,Language*, andUser*create/delete method) now declaresEntityStubInterface. Migration: replace(object) ['type' => 'page', ...]constructs withnew \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\stdClassshim - this is a clean cut. - Added 'FieldClassifier' with nine F-row predicates and rewrote field-expansion pipeline. (#359)
FieldCapabilityInterfaceis removed along withfieldExists()andfieldIsBase()onCoreandDrupalDriverInterface; both predicates now live onFieldClassifierInterface.Core::expandEntityFields()no longer accepts a$base_fieldsargument andCore::getEntityFieldTypes()now takes?string $bundleinstead ofarray $base_fields.Core::expandEntityBaseFields()andCore::detectBaseFieldsOnEntity()were removed.DefaultHandler::expand()now throws\RuntimeExceptionfor any field whose storage is not a singlevaluecolumn instead of silently emitting garbage. Migration is documented inUPGRADING.md. - Tightened 'DrupalDriverInterface' contract and 'DrupalDriver' visibility. (#358)
getCore(),setCore(), andgetDrupalVersion()are now part of theDrupalDriverInterfacecontract, so any consumer that implements the interface directly must add these three methods.DrupalDriver::$coreandDrupalDriver::$versionwere narrowed frompublictoprotected; 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 fromsetCore(array $available_cores)(keyed by Drupal version) tosetCore(CoreInterface $core). Migration: callers that previously passed a version-keyed array of core classes must now construct or select a singleCoreInterfaceinstance themselves and inject it. The convention-basedsetCoreFromVersion()lookup that walksDrupal\Driver\Core{N}\Coreclasses is unchanged. - [#154] Removed unused 'TaxonomyTermReferenceHandler' and its tests. (#352)
The handler matched the legacytaxonomy_term_referencefield type that Drupal core removed in 8.0.0-beta10 (2015) and was unreachable on every supported Drupal version. Modern taxonomy-linking fields areentity_referencewithtarget_type = taxonomy_termand route throughEntityReferenceHandler; consumers that subclassedTaxonomyTermReferenceHandlershould subclassEntityReferenceHandlerinstead. - [#270] Fixed 'entityCreate()' base field expansion and dynamic id key resolution. (#346)
entityCreate()now populates the entity-type-specific id property (e.g.$stub->uidfor users,$stub->nidfor nodes) instead of always assigning to$stub->id, andentityDelete()reads from the same key. Consumers reading$stub->idafter a genericentityCreate('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)
DrushDriverInterfaceno longer extendsContentCapabilityInterfaceorFieldCapabilityInterface.entityCreate,entityDelete,nodeCreate,nodeDelete,termCreate,termDelete,fieldExists, andfieldIsBaseare gone fromDrushDriver, and thedrush-ops/behat-drush-endpointcompanion module is no longer required. Consumers that need entity CRUD or field introspection should switch toDrupalDriver; seeUPGRADING.mdfor the per-method migration table. - [v3] Restructured driver and core contracts into composable capability interfaces. (#342)
The monolithicDriverInterface/BaseDrivermodel was replaced with twelve fine-grainedCapability\*CapabilityInterfaceinterfaces and per-driver composite interfaces (DrupalDriverInterface,DrushDriverInterface,BlackboxDriverInterface). Many methods were renamed to lead with their capability prefix (createNode→nodeCreate,createTerm→termCreate,createEntity→entityCreate,isField→fieldExists,isBaseField→fieldIsBase,clearCache→cacheClear,clearStaticCaches→cacheClearStatic,runCron→cronRun,fetchWatchdog→watchdogFetch, all*Mailmethods →mail*).BaseDriver,AbstractCore,AuthenticationDriverInterface,CoreAuthenticationInterface, andDrupalCoreInterfacewere removed.UPGRADING.mddocuments every rename and theinstanceofcapability-check pattern. - Restructured cores into 'Core/' with version-override lookup chain. (#338)
src/Drupal/Driver/Cores/Drupal8.phpwas renamed tosrc/Drupal/Driver/Core/Core.php(classDrupal8→Core) andsrc/Drupal/Driver/Fields/Drupal8/*moved tosrc/Drupal/Driver/Core/Field/*. Any code referencingDrupal\Driver\Cores\Drupal8,Drupal\Driver\Fields\Drupal8\*, or the oldCores/andFields/namespaces must update toDrupal\Driver\Core\CoreandDrupal\Driver\Core\Field\*. A future Drupal version that diverges can ship aDrupal\Driver\Core{N}\Coresubclass and thesetCoreFromVersion()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 hitTypeErrorat the boundary; audit any consumer code that calls intoCore,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 aBootstrapExceptionwhen 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 the2.xbranch).
All changes
- [#362] Replaced 'stdClass' entity stubs with typed 'EntityStub' class. (#363)
Every capability interface that touched\stdClassnow declaresEntityStubInterface, the field-handler boundary accepts the typed stub directly, and the previous(object) [...]constructs are replaced withnew 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 thedrupalextensiontest 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 withoutTextHandlerandTextLongHandler, both multi-column types that immediately tripped the new loud-failure policy inDefaultHandler. This adds both handlers, restores the 2.x reuse-by-URI / reuse-by-basename behaviour forFileHandler(andImageHandler, which now extendsFileHandler), and addsFieldTypeCoverageKernelTestthat 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)
FieldClassifierexposes 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, andDefaultHandlerthrows for any multi-column type without a registered handler.EntityReferenceRevisionsHandleris added for paragraphs and other revision-tracked references;FieldCapabilityInterfaceand thefieldExists/fieldIsBasepredicates are removed. - Tightened 'DrupalDriverInterface' contract and 'DrupalDriver' visibility. (#358)
getCore(),setCore(), andgetDrupalVersion()are now declared onDrupalDriverInterface, and$core/$versiononDrupalDriverwere narrowed frompublictoprotected. Consumers that hand-rolled aDrupalDriverInterfaceimplementation 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(), andblockContentDelete()are now first-class capability methods onDrupalDriverandCore. Placement covers theblockconfig entity (id auto-generated when omitted, plugin/theme/region wiring), and the content-block methods cover theblock_contentcontent 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 anyCoreInterfaceimplementation. InternallyCore::__construct()callsregisterDefaultFieldHandlers()which scans the siblingField/directory and registers every concrete*Handlerclass, so future Drupal versions add handlers additively by dropping a file into aCore{N}\Field\directory. - [#154] Removed unused 'TaxonomyTermReferenceHandler' and its tests. (#352)
The handler matched the legacytaxonomy_term_referencefield type that Drupal core removed in 8.0.0-beta10 (2015) and was unreachable on every supported Drupal version. Modern taxonomy references go throughEntityReferenceHandler(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()andentityDelete()now throwInvalidArgumentException('Unknown entity type "X".')when given a bad type instead of leakingPluginNotFoundException.termCreate()validates thevocabulary_machine_nameproperty, 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 readson_label/off_labelfrom the field config and matches them case-insensitively, then falls back tofilter_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 aRuntimeExceptionlisting 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$idis given, it doubles as the human-readable label. - Decoupled dev dependencies from 'drupal/core-dev' metapackage. (#348)
Thedrupal/core-devrequirement was replaced with the three transitive deps the test suite actually needs (vfsstream,prophecy-phpunit,phpstan/extension-installer), and a project-ownedtests/bootstrap.phpreplaces 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, andentity_reference_revisionsuse -['target_id' => 'alice', 'display' => 1]) previously crashed withCalling 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 likecommerce_product.variationsresolve their labels instead of reaching storage raw.entityCreate()andentityDelete()now resolve the id key per entity type (uid,nid,tid, etc.) instead of hardcodingid. - Refactored source files for readability. (#345)
Vertical-rhythm pass over twelve source files insrc/Drupal/Driver/: guard clauses replace nested conditionals, blank lines bracketif/foreach/forblocks, generic$returnvariables are renamed to semantic names, and step-by-step comments that just restated the code are removed. One incidental correctness fix inDrushDriver::drush()replaces an!== NULLcheck on a typed non-nullable property withisset()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 onDrupalDriverandDrushDriverare now grouped alphabetically by capability (Authentication, Cache, Config, Content, ...) so navigating between the two drivers is consistent.$bootstrapped,$drupalRoot,$uri,$random, and$argumentswere widened fromprivatetoprotectedso 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)
DrushDriverInterfaceno longer extendsContentCapabilityInterfaceorFieldCapabilityInterface, and the eight methods that required the companion endpoint module (entityCreate,entityDelete,nodeCreate,nodeDelete,termCreate,termDelete,fieldExists,fieldIsBase) are removed fromDrushDriver. Consumers that need those operations should useDrupalDriver; seeUPGRADING.md. - [v3] Restructured driver and core contracts into composable capability interfaces. (#342)
Twelve fine-grainedCapability\*CapabilityInterfaceinterfaces replace the monolithicDriverInterface/BaseDrivermodel; each driver opts into the capabilities it actually services. Many methods were renamed to lead with their capability (createNode→nodeCreate,clearCache→cacheClear,runCron→cronRun,isField→fieldExists, the*Mailfamily flipped tomail*);BaseDriver,AbstractCore,AuthenticationDriverInterface,CoreAuthenticationInterface, andDrupalCoreInterfaceare removed. - Added kernel test infrastructure for field handler round-trips. (#341)
A sharedFieldHandlerKernelTestBasewithattachField()andassertFieldRoundTripViaDriver()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, andCore::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 viashivammathur/setup-php@v2, withactions/cachefor the Composer cache. Thedocker-compose.ymland thewodby/drupal-phpimage dependency are gone, removing thePHP_VERSION/DOCKER_USER_IDenv scaffolding the container needed. - Removed 'doc/' in favour of using markdown in repo. (#339)
The Sphinx tree (Makefile,conf.py, eight.rstfiles, snippets) and the stale ReadTheDocs link are removed; the architecture/extending guide that lived indoc/extending.rstis condensed into a new "Extending for a new Drupal version" section inCONTRIBUTING.md. - Restructured cores into 'Core/' with version-override lookup chain. (#338)
src/Drupal/Driver/Cores/Drupal8.phpbecomessrc/Drupal/Driver/Core/Core.phpand the seventeen handlers underFields/Drupal8/move toCore/Field/.DrupalDriver::setCoreFromVersion()andCore::getFieldHandler()walk aCore{N}\chain so a future Drupal version that diverges can ship an override file alongside the default without copy-pasting the whole implementation. NoCore{N}/directory exists today; everything resolves to the defaultCore\CoreandCore\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-rectorwas dropped in favour ofrector/rector ^2.0directly). 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-lowestand normal dependency resolution.masteris the active 3.x branch; the maintenance line lives on the new2.xbranch. - Removed 'CHANGELOG.md' in favour of GitHub Releases. (#335)
The 199-line in-repo changelog was redundant with thedraft-release-notes.ymlworkflow that already publishes notes via GitHub Releases. The "Release notes" link inREADME.mdnow points at the Releases page. - Split 'lint' job out of 'tests' job in CI workflow. (#334)
Lint andcomposer normalize --dry-runwere moved into a dedicated job that runs once on PHP 8.4 / Drupal 11 instead of repeating across every matrix combination. Thetestsjob 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)
- [#366] Made 'NameHandler' honour the field's 'components' setting. @AlexSkrypnyk (#368)
- [#365] Added creation alias registry for entity stub properties. @AlexSkrypnyk (#369)
Full Changelog: v3.0.0-alpha1...v3.0.0-rc1
v3.0.0-rc2 (May 19, 2026)
All changes
- Added 'SmartdateHandler' for the
smart_datecontrib module. @AlexSkrypnyk (#371)
Closes a v3.0 regression: writing to asmartdatefield (six storage columns from thesmart_datecontrib module) threwRuntimeExceptionbecause v3.0 hardenedDefaultHandlerto 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 andstrtotime()-compatible strings; and auto-derivesdurationas(end - start) / 60clamped to zero when both endpoints are supplied without an explicit duration. - Fixed 'ImageHandler' and 'DatetimeHandler' compound parser shape. @AlexSkrypnyk (#370)
Fixes aTypeErrorinImageHandler::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 sharednormalise()helper onAbstractHandlerthat 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 migratesImageHandler,DatetimeHandler, and the three text handlers onto it. The ambiguous "positional value + named extras" shape (e.g.['/path', 'alt' => 'A']) is now rejected withInvalidArgumentExceptioninstead of being silently disambiguated.
Full Changelog: v3.0.0-rc1...v3.0.0-rc2