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 start
Then install the add-on:
ddev add-on get amateescu/ddev-drupal-dev
ddev restart
ddev composer install
Use ddev add-module to clone a contrib module for development:
ddev auth ssh # forward SSH keys (needed once per session)
ddev add-module token
ddev add-module token 2.0.x # specific branch
ddev add-module --https token # or use HTTPS (no push access)
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 token
ddev remove-module token
This 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/pathauto
Core’s composer.json and composer.lock are never modified by the overlay. You work on core normally: edit files, run tests, commit, create patches.
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 tests
For 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/phpstan
Inside 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.sh
An .envrc file is created during installation. If you have direnv installed, run:
direnv allow
This 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 |
composer.local.json file lives in the core root (ignored via .gitignore).wikimedia/composer-merge-plugin, which pulls in everything from core’s composer.json.COMPOSER env var is set to composer.local.json inside the DDEV web container, so Composer reads the overlay instead of core’s file.vendor/ and autoloader with both core’s deps and your extras, while core’s composer.json and composer.lock remain untouched.drupal-dev/composer-git-installer) intercepts installs for drupal-module, drupal-theme, and drupal-profile packages. If a .git directory already exists at the install path, the download is skipped and the package is registered in the installed repository so autoloading works correctly.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"]
}
}
}
composer installIt’s harmless. It just overwrites vendor/ with only core’s deps, losing any overlay packages until re-installed. Fix it with:
ddev composer install
When 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.
Contributed and maintained by @amateescu