Don’t Pin Your Drupal Dev Tools
October 26, 2026
I’ve been upgrading an older Drupal site from 10.1 to 11.4. The core upgrade itself went fine. The fights were in require-dev.
Here’s what that site’s dev dependencies looked like when I started:
"require-dev": {
"drupal/coder": "^8.3",
"mglaman/drupal-check": "^1.4",
"mikey179/vfsstream": "^1.6",
"palantirnet/drupal-rector": "^0.15.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.7",
"symfony/phpunit-bridge": "^6.2",
"zaporylie/composer-drupal-optimizations": "^1.2",
...
}
Every one of those is pinned by hand, and every one of them blocked something along the way. PHPUnit 9 held sebastian/diff back, so Drupal 11 wouldn’t resolve. PHPStan 1 blocked the new Drupal Rector. One plugin only made sense on Composer 1.
There’s a better way, and it’s been in core since the Drupal 8 days: drupal/core-dev.
What core-dev is
drupal/core-dev is a metapackage. It has no code, only a list of requirements: the testing and analysis tools that Drupal core itself is tested with, at versions that work with that core release.
For Drupal 11.4 that includes PHPUnit 11.5, PHPStan with phpstan-drupal, coder (which brings PHP_CodeSniffer), Prophecy, vfsStream, and Mink for functional tests.
The payoff is this: once core-dev is in require-dev, the usual core update command moves your tools too, because drupal/core-* matches drupal/core-dev.
composer update "drupal/core-*" --with-all-dependencies
No more separate PHPUnit upgrade project every time core moves.
The timing trap
My first thought was to switch on Drupal 10, before the core upgrade. That would have been a mistake.
I’d already moved to Drupal Rector 1.x to fix deprecated code, and Rector 1.x needs PHPStan 2. Here’s what core-dev requires along the way:
| core-dev | PHPStan | PHPUnit |
|---|---|---|
| 10.1.2 | ^1.10 | ^9.5 |
| 10.6.20 | ^1.12 | ^9.6 |
| 11.0.0 | ^1.11 | ^10.5 |
| 11.2.0 | ^2.1 | ^10.5 or ^11.5 |
| 11.4.11 | ^1.12.27 or ^2.2 | ^11.5.50 |
Every core-dev release for Drupal 10, plus 11.0 and 11.1, requires PHPStan 1. Switch early and Composer makes you downgrade Rector, right when you need it most.
So the order I’d recommend:
- On Drupal 10, update your tools individually, as far as they’ll go.
- Upgrade core to Drupal 11 (see my Drupal 10 to 11 checklist).
- Once you’re on 11.2 or later, switch to core-dev.
The switch
First, work out what core-dev covers. In my case:
- Remove (core-dev provides them):
drupal/coder,squizlabs/php_codesniffer,phpunit/phpunit,phpspec/prophecy,phpspec/prophecy-phpunit,phpstan/phpstan,mikey179/vfsstream - Remove (no longer needed):
symfony/phpunit-bridge. core-dev 11.4 doesn’t include it, and Drupal 11 runs on PHPUnit 11 without it. - Keep (not in core-dev):
drupal/devel,drupal/upgrade_status,palantirnet/drupal-rector,phpstan/phpstan-deprecation-rules,phpstan/phpstan-strict-rules
Then change the constraints first, and resolve everything in one update:
composer remove --dev drupal/coder squizlabs/php_codesniffer phpunit/phpunit \
phpspec/prophecy phpspec/prophecy-phpunit phpstan/phpstan mikey179/vfsstream \
symfony/phpunit-bridge --no-update
composer require --dev "drupal/core-dev:^11.4" --no-update
composer update drupal/core-dev drupal/coder squizlabs/php_codesniffer phpunit/phpunit \
phpspec/prophecy phpspec/prophecy-phpunit phpstan/phpstan mikey179/vfsstream \
symfony/phpunit-bridge --with-all-dependencies
--no-update only edits composer.json. Listing the removed packages in the final update lets Composer bring them back in through core-dev, or drop them if nothing needs them anymore.
What to expect
- Plugin prompts. core-dev pulls in two Composer plugins:
php-http/discoveryandtbachert/spi(from the OpenTelemetry packages). Composer asks whether to trust them. Say yes. They end up inallow-plugins. - The same versions, mostly. I ended up with PHPUnit 11.5.57, PHPStan 2.3.2 and coder 8.3.31. Note that coder stays on 8.x. Coder 9 needs PHP_CodeSniffer 4, and core isn’t there yet, so don’t fight it.
- A smaller
require-dev. Mine went from eight testing lines to one.
Afterwards, check that the tools still run:
vendor/bin/phpunit --version
vendor/bin/phpstan --version
vendor/bin/phpcs -i # Drupal and DrupalPractice should be listed
If your editor runs phpcs on save, check that too. It now uses the coder that core-dev installed.
Summary
drupal/core-dev isn’t new, but the Drupal 10 to 11 upgrade is the right moment to adopt it. Wait until you’re on Drupal 11.2 or later, so its PHPStan 2 matches Drupal Rector 1.x. Then remove the tools it covers and require it in one go.
From then on, your test tooling updates when core does. That’s one less thing to fight when Drupal 12 arrives.
