Declare Drupal 12 compatibility and turn on the next-major test lane
Turns the next-major lane on, fixes what it reports, and widens the core constraint. Reconciles the generated changes from [#3595431] rather than applying them blind.
Deprecations removed in Drupal 12
Both families were deprecated in 11.2, so the replacements exist on the module's ^11.3 floor:
REQUIREMENT_*constants becomeRequirementSeverityenum cases.\PDO::FETCH_ASSOCbecomesFetchAs::Associative.
Three things the bot patch got wrong or left behind:
RequirementsHook::runtime()compared$entry['severity'] ?? 0against an enum case. An int default can never equal an enum, so that arm had quietly stopped meaning anything; now?? NULL.- Generated
usestatements were prepended rather than sorted in. CronVerifyTest::setUp()pulled incore/includes/install.incpurely for the constants. The enum autoloads, so the require is gone, along with docblocks naming the old constants.
The real Drupal 12 signature break
ConfigEntityListBuilder::getDefaultOperations() takes a required $cacheability in Drupal 12; 11.3 declares it commented-out and reads it via func_get_args(). The three list builders and hook_entity_operation now carry the parameter, with $cacheability ??= new CacheableMetadata() so the parent call is valid on both.
Test suite under PHPUnit 12
The Drupal 12 lane runs PHP 8.5 / PHPUnit 12, which is stricter than the current lane's PHP 8.3 / PHPUnit 11. Two consequences:
- Six
createMock()calls that never set an expectation are nowcreateStub(). Each mock was checked forexpects()first; the other 21 stay mocks. AuditTrailLoggerreported a dropped chained write with a bareerror_log(). Under process isolation PHPUnit captures that off the child's stream and marks the test risky, and it cannot be suppressed from inside the test: anerror_log()write escapes PHPUnit's output buffer, so neither an output expectation nor a nested buffer intercepts it (phpunit#2155, phpunit#1452). Redirecting theerror_logini works up to PHP 8.4 but not on 8.5, where the value is honored yet the write still goes to the SAPI logger.
So the report now goes throughDroppedEventReporterInterface. The defaultErrorLogDroppedEventReporterstill callserror_log(), so operator-visible behavior and the message text are unchanged; the service is dependency-free, which keeps the logger's graph clear oflogger.factory(the [#3591791] cycle constraint). The kernel test registers a capturing reporter and now asserts the reports instead of hiding them: two drops, two reports, each naming the chain.
Pre-existing phpstan findings
44 findings unrelated to Drupal 12, which fail on 1.x today: phpstan-drupal 2.1.1 added the rules after the last green 1.x run on 2026-07-02, and the job is allow_failure, so it was red under a green badge. Fixed here because the next-major job reports the same set: promoted service properties on form and list-builder subclasses drop private/readonly for protected (DependencySerializationTrait cannot restore them), and the three list builders take EntityTypeManagerInterface and call getStorage() in the constructor, which is what the rule asks for and what createInstance() already did.
CI and version constraints
OPT_IN_TEST_NEXT_MAJOR: 1._LENIENT_ALLOW_LIST: 'key,paragraphs'— key pins^9.1 || ^10 || ^11, paragraphs^10.3 || ^11; without them the next-major lane cannot resolve. Each entry comes out when that project declares^12.drush/drush: ^13.0 || ^14— drush 13 caps symfony/yaml at^7while core main needs~8.1, which blocked the lane before anything installed. 14.x-dev allows^7 || ^8.core_version_requirement: ^11.3 || ^12on all six info files,drupal/core: ^11.3 || ^12in composer.json. README and the docs landing page said "for Drupal 11".
Verification
Every job green, read from the job logs rather than the badges, since phpcs / phpstan / cspell / stylelint are all allow_failure here. phpstan (next major): no errors. phpunit (next major): 448 tests, 2056 assertions, no failures, no risky tests. Locally: full kernel suite 31/31 on MySQL, phpcs clean across the module.
A new alpha still has to follow. Consumers pin ^1.0@alpha, which resolves to released tags, so a widened 1.x branch alone leaves them as blocked as before.