A DDEV add-on for working on Drupal core and contrib modules together, using a core git checkout as the project root.
Other add-ons target either core or contrib in isolation. This one is for when you need both: developing a contrib module against the latest core, fixing a core bug that affects contrib, or running contrib tests on a core patch.
Extra dependencies (contrib modules, Drush, dev tools) are managed through a composer.local.json overlay, keeping core's composer.json and composer.lock untouched.
Clone Drupal core and configure DDEV to use it as the project root:
git clone https://git.drupalcode.org/project/drupal.git drupal-dev
cd drupal-dev
ddev config --project-type=drupal12
ddev startThen install the add-on:
ddev add-on get amateescu/ddev-drupal-dev
ddev restart
ddev composer installUse ddev add-module to clone a contrib module for development:
ddev add-module token
ddev add-module token 2.0.x # specific branch
ddev add-module --https token # or use HTTPS (no push access)The clone runs on your host, so it uses your host SSH keys directly; no ddev auth ssh needed.
This clones the module into modules/contrib/, registers it as a path repository in composer.local.json, and runs composer require, all in one step.
The module is a preserved git checkout. Composer detects the .git directory and skips re-downloading it. Its dependencies are resolved through the overlay, keeping core's files untouched.
You can work on multiple modules this way; each gets its own git checkout that you can commit and push to independently.
The overlay includes composer-drupal-lenient, so contrib modules that don't yet declare compatibility with your core version (e.g. working on main/12.x-dev with a module that only supports ^11) will still install.
After switching a module's git branch, update the Composer constraint to match:
cd modules/contrib/token && git checkout 2.0.x && cd -
ddev update-module tokenddev remove-module tokenThis removes the composer requirement, unsets the path repository, and deletes the cloned directory. It will abort if the module has uncommitted changes.
If you just need a module as a dependency (not for active development), require it directly:
ddev composer require drupal/pathautoCore's composer.json and composer.lock are never modified by the overlay. You work on core normally: edit files, run tests, commit, create patches.
When reproducing a core bug or validating a patch, you sometimes need the same resolved dependency versions as core's composer.lock. By default the overlay's solver runs fresh, so shared packages (Symfony, Guzzle, etc.) may resolve to newer versions than core recorded.
Enable pinning to keep every shared package at core's locked version:
{
"extra": {
"drupal-dev": {
"pin-core-lock": true
}
}
}Then ddev composer update to re-solve with pinning applied. Packages that appear in core's composer.lock are pinned to the exact version (and commit SHA, for dev refs); overlay-only packages resolve normally. Subsequent ddev composer install runs replay the pinned composer.local.lock unchanged.
Pinning affects the solve step, so after enabling (or disabling) the flag you need to run ddev composer update once to regenerate composer.local.lock. Core's lock is re-read on every solve, so there is no separate refresh step.
Disable it by setting the flag to false or removing the key, then ddev composer update.
Tests run against your project's configured database by default. Use --db to switch:
ddev phpunit core/modules/node # project database (default)
ddev phpunit --db=sqlite core/modules/node # SQLite
ddev phpunit --db=pgsql core/modules/node # PostgreSQL
ddev phpunit modules/contrib/token # contrib module testsFor PostgreSQL, install the ddev-postgres add-on first.
Any package can be added through the overlay:
ddev composer require drush/drush
ddev composer require --dev phpstan/phpstanInside DDEV, ddev composer always uses the overlay automatically. On the host, bare composer, drush and php will bypass DDEV. To prevent that, use one of these options:
The add-on includes a shell helpers script that wraps composer, drush, php and phpunit, automatically delegating to DDEV when you're inside a DDEV project and falling back to the host binary otherwise.
Add this to your ~/.bashrc or ~/.zshrc:
source /path/to/your/project/.ddev/drupal-dev/shell-helpers.shAn .envrc file is created during installation. If you have direnv installed, run:
direnv allowThis sets the COMPOSER env var on the host so that running composer directly on the host uses the overlay. Note that direnv cannot export shell functions, so you still need the shell helpers above for composer, drush, php and phpunit delegation.
| Command | Description |
|---|---|
ddev phpunit [path] |
Run PHPUnit tests |
ddev add-module <name> |
Clone a contrib module for development |
ddev update-module <name> |
Update composer constraint after switching a module's branch |
ddev remove-module <name> |
Remove a previously cloned contrib module |
- A
composer.local.jsonfile lives in the core root (ignored via.gitignore). - It requires
wikimedia/composer-merge-plugin, which pulls in everything from core'scomposer.json. - The
COMPOSERenv var is set tocomposer.local.jsoninside the DDEV web container, so Composer reads the overlay instead of core's file. - Result: a unified
vendor/and autoloader with both core's deps and your extras, while core'scomposer.jsonandcomposer.lockremain untouched. - A custom Composer plugin (
drupal-dev/composer-git-installer) intercepts installs fordrupal-module,drupal-theme, anddrupal-profilepackages. If a.gitdirectory already exists at the install path, the download is skipped and the package is registered in the installed repository so autoloading works correctly. - When
extra.drupal-dev.pin-core-lockis enabled, the same plugin subscribes to Composer's pre-pool-create event and filters the solver's candidate pool against core'scomposer.lock, so shared packages can only resolve to their locked versions.
Only composer.local.json and composer.local.lock are written (both ignored via .gitignore).
By default, modules are installed into modules/contrib/ (the standard Drupal layout). Both ddev add-module and the Composer plugin read the installer-paths from your Composer configuration, so you can change the layout by overriding it in composer.local.json:
{
"extra": {
"installer-paths": {
"modules/{$name}": ["type:drupal-module"]
}
}
}It's harmless. It just overwrites vendor/ with only core's deps, losing any overlay packages until re-installed. Fix it with:
ddev composer installWhen upgrading the add-on, your composer.local.json is preserved (it contains your modules and custom packages). If a new version of the add-on introduces changes to the base composer.local.json, check .ddev/drupal-dev/composer.local.json for any new dependencies and add them manually.
- ddev-drupal-core-dev -- Core development only. Same project layout (core git checkout), but no contrib module or Composer management. Use this if you only work on core.
- ddev-drupal-contrib -- Single contrib module development. Core is pulled in as a Composer dependency. Use this if you work on one contrib module and don't need a core checkout.
- ddev-drupal-suite -- Multiple contrib modules. Similar to ddev-drupal-contrib but supports working on several modules at once. Core is a dependency, not a checkout.
Contributed and maintained by @amateescu