Skip to content

mandclu/ddev-module-developer

Repository files navigation

add-on registry tests last commit release

DDEV Module Developer

DDEV add-on providing code quality validation commands for Drupal module development. The commands and their default configurations match the Drupal GitLab CI template from the Drupal Association, so code that passes locally will pass in CI.

All tools are installed at container build time — they are available immediately after ddev restart with no per-start installation overhead.

Related add-ons and overlap

There is overlap with other Drupal-focused DDEV add-ons. The key difference is the project type each one is designed for.

Add-on Best for Typical project layout
UltraBob/ddev-drupal-code-quality Full Drupal website projects where your site repo already contains Drupal code and custom code. Existing site/project repo; installs code-quality configs and IDE shims in-place.
mandclu/ddev-module-developer Drupal contrib module/theme development but compatible with working on multiple projects in a single DDEV environment. Standard Drupal or Drupal CMS install, with contrib projects cloned in manually or using other strategies like the --prefer-source composer flag.
ddev/ddev-drupal-contrib Drupal contrib module/theme development where the contrib project is the center of the repo. Contrib project repo with Drupal scaffolded around it (symlink workflow).
justafish/ddev-drupal-core-dev / joachim-n/ddev-drupal-core-dev Drupal core development. Drupal core checkout or core-dev project template.

Install

  1. If you haven't already, install Docker and DDEV.
  2. Set up a Drupal project with DDEV, either by following the Drupal CMS quickstart or the Drupal core quickstart.
  3. Add this add-on and restart:
ddev add-on get mandclu/ddev-module-developer
ddev restart
  1. Optional — for eslint and stylelint to use Drupal core's own configuration files (the same ones used in CI), install core's JavaScript dependencies:
ddev exec "cd web/core && yarn install"

Without this step both commands still work using the bundled fallback configurations.

Commands

This add-on provides the following DDEV commands, all running inside the web container.

  • ddev checks — Run all Drupal GitLab CI checks in sequence and print a summary. See Running all checks below.
  • ddev checks-fixes — Auto-fix code style violations by running phpcbf, eslint --fix, and stylelint --fix in sequence. See Automatically fix coding standard violations below.
  • ddev parallel-lint — Run php-parallel-lint to check PHP files for syntax errors.
  • ddev phpcs — Run PHP_CodeSniffer against Drupal coding standards.
  • ddev phpcbf — Auto-fix phpcs violations using PHP Code Beautifier and Fixer.
  • ddev phpstan — Run PHPStan static analysis with the Drupal extension.
  • ddev phpmd — Run PHP Mess Detector.
  • ddev rector — Run Drupal Rector to find and fix deprecated API usage.
  • ddev phpunit — Run PHPUnit tests.
  • ddev stylelint — Run Stylelint on CSS/SCSS files.
  • ddev eslint — Run ESLint on JavaScript and YAML files, with Prettier formatting checks.
  • ddev cspell — Run CSpell spell-checking across project files.

Pass a path as the first argument to any command to target a specific file or directory:

ddev checks web/modules/custom/mymodule
ddev parallel-lint web/modules/custom/mymodule
ddev phpcs web/modules/custom/mymodule
ddev phpstan analyse web/modules/custom/mymodule
ddev phpunit web/modules/custom/mymodule
ddev stylelint 'web/modules/custom/mymodule/**/*.css'
ddev eslint web/modules/custom/mymodule/js
ddev phpmd web/modules/custom/mymodule text

Run without a path argument from inside a module directory to target that module:

cd web/modules/custom/mymodule
ddev checks
ddev phpcs
ddev phpunit

Absolute host paths

In addition to relative paths, every command accepts an absolute path on the host as an argument and rewrites it to the equivalent path inside the container. This is convenient for IDE "external tools", file watchers, and AI agents that naturally work with full host paths:

ddev phpcs /Users/me/Sites/myproject/web/modules/custom/mymodule
ddev phpstan analyse /Users/me/Sites/myproject/web/modules/custom/mymodule

This behavior comes from module-developer/lib/init.sh, which every command sources as its first step (init.sh is also the home for any future shared utilities). The match is host-root agnostic — because the host project path is not known inside the container, it strips leading path components until the remainder resolves under the project root, choosing the longest (most specific) match. It is a no-op for relative paths, for flags, and for absolute paths that do not resolve under the project root.

Note

Two limitations follow from matching by suffix:

  • Glob wildcards (e.g. /abs/path/**/*.css for stylelint) are left untouched, since the path cannot be matched as it stands. Pass those patterns relative to the project root instead.
  • Coincidental suffixes: an absolute path that does not exist in the container but whose trailing component happens to equal a top-level project entry (e.g. /unrelated/web matching the docroot) is rewritten to that entry rather than passed through. For paths that are not inside the project, pass them relative or cd to them rather than passing an unrelated absolute path.

Running all checks

ddev checks runs the same sequence of code quality jobs that the Drupal GitLab Templates pipeline runs, in the same order:

  1. PHP Lint (parallel-lint) — fast syntax check; catches parse errors before heavier tools run.
  2. PHP CodeSniffer — Drupal coding standards.
  3. PHPStan — static analysis.
  4. ESLint — JavaScript and YAML formatting.
  5. Stylelint — CSS/SCSS validation.
  6. CSpell — spell checking.
  7. PHPUnit — unit and functional tests, but only when all preceding checks pass and the Drupal test bootstrap is installed. Automatically skipped otherwise.

Each tool prints its normal output inline. At the end, a summary shows the result for every check:

══════════════════════════════════════════════════════
  ✓  PHP Lint
  ✓  PHP CodeSniffer
  ✓  PHPStan
  ✓  ESLint
  ✓  Stylelint
  ✓  CSpell
  -  PHPUnit  (skipped — Drupal test bootstrap not installed)

Result: passed  (1 skipped, 6 passed)

ddev checks exits 0 when all checks pass and 1 when any check fails.

Bonus checks

Pass --bonus to also run checks that are not part of the Drupal GitLab CI pipeline but are available in this add-on:

ddev checks --bonus
ddev checks --bonus web/modules/custom/mymodule

Bonus checks run after the CI checks and do not affect the phpunit gate. They are treated as hard failures (no allow_failure equivalent):

  • PHP Mess Detector — code quality and complexity metrics.
  • PHP Compatibility — detects PHP version compatibility issues.

Warnings and allow_failure

Some projects configure certain CI jobs to allow failure (for example, during a transition period). This add-on respects the same variables the Drupal GitLab CI pipeline uses. Set them in the variables: block of your .gitlab-ci.yml:

Variable Effect
_ALL_VALIDATE_ALLOW_FAILURE: "1" All CI validation checks show failures as warnings
_PHPCS_ALLOW_FAILURE: "1" phpcs failures show as warnings
_PHPSTAN_ALLOW_FAILURE: "1" phpstan failures show as warnings
_ESLINT_ALLOW_FAILURE: "1" eslint failures show as warnings
_STYLELINT_ALLOW_FAILURE: "1" stylelint failures show as warnings
_CSPELL_ALLOW_FAILURE: "1" cspell failures show as warnings
_PHPUNIT_ALLOW_FAILURE: "1" phpunit failures show as warnings

When a check is configured to allow failure and it fails, it shows as in the summary. If every failure was a warning (no hard failures), ddev checks exits 0 with Result: passed with warnings.

Per-tool variables take precedence over _ALL_VALIDATE_ALLOW_FAILURE, matching how GitLab CI applies these settings.

Skipping checks

ddev checks also respects SKIP_* variables from .gitlab-ci.yml. A skipped check is shown as - in the summary and does not affect the exit status:

Variable Effect
SKIP_COMPOSER_LINT: "1" Skips parallel-lint
SKIP_PHPCS: "1" Skips phpcs
SKIP_PHPSTAN: "1" Skips phpstan
SKIP_ESLINT: "1" Skips eslint
SKIP_STYLELINT: "1" Skips stylelint
SKIP_CSPELL: "1" Skips cspell
SKIP_PHPUNIT: "1" Skips phpunit

PHPUnit prerequisites

ddev phpunit requires drupal/core-dev to be installed in your project. If it is not already present, add it with:

ddev composer require --dev drupal/core-dev -W

The -W flag (--with-all-dependencies) is needed to allow Composer to adjust the versions of shared packages (such as sebastian/diff) to satisfy phpunit's constraints alongside those of drupal/core.

If your project has a phpunit.xml or phpunit.xml.dist in the project root, ddev phpunit uses it as-is. Without one, the command bootstraps from web/core/tests/bootstrap.php and sets the following DDEV-standard defaults so all test types work out of the box:

Environment variable Default value
SIMPLETEST_BASE_URL $DDEV_PRIMARY_URL
SIMPLETEST_DB mysql://db:db@db/db
BROWSERTEST_OUTPUT_DIRECTORY web/sites/simpletest/browser_output

Override any of these by setting them in .ddev/config.yaml under web_environment before running the command.

By default, ddev phpunit passes --display-all-issues to PHPUnit so that deprecations, notices, and warnings are shown inline rather than summarised. To suppress this output, set PHPUNIT_DISPLAY_ALL_ISSUES=0 in .ddev/config.yaml:

web_environment:
  - PHPUNIT_DISPLAY_ALL_ISSUES=0

Configuration

Each command follows a three-level priority for its configuration:

  1. Project config — if a config file exists in your project root (e.g. phpcs.xml, phpstan.neon, .stylelintrc.json), it is used as-is.
  2. Drupal core config — for eslint and stylelint, if core/node_modules is installed, the command uses core/.eslintrc.passing.json and core/.stylelintrc.json respectively — the same files used in the GitLab CI pipeline.
  3. Bundled defaults — if neither of the above is found, the add-on's own default configs in .ddev/module-developer/config/ are used. These match the Drupal GitLab Templates defaults.

Overriding a tool's configuration

Place a config file in your project root to override the bundled default for any tool:

Tool Config file(s)
phpcs / phpcbf phpcs.xml or phpcs.xml.dist
phpstan phpstan.neon or phpstan.neon.dist
rector rector.php or rector.php.dist
stylelint .stylelintrc.json (or any supported format)
eslint .eslintrc.json (or any supported format)
cspell .cspell.json

The bundled phpcs.xml matches the GitLab Templates default: only the Drupal standard is enforced; DrupalPractice is commented out and can be enabled if required.

The bundled phpstan.neon runs at level 0, matching the GitLab Templates default. To raise the level, add a phpstan.neon to your project root:

parameters:
  level: 2

PHPStan and the Drupal extension

The Drupal GitLab CI Docker image pre-installs mglaman/phpstan-drupal, so the template's phpstan.neon does not reference it. In DDEV there is no such pre-built image, so when no project-level config is found the phpstan command automatically injects the extension. If you supply your own phpstan.neon and want the Drupal extension, add the includes explicitly:

includes:
  - /usr/local/composer/vendor/mglaman/phpstan-drupal/extension.neon
  - /usr/local/composer/vendor/mglaman/phpstan-drupal/rules.neon

Ignoring files

Most tools support an ignore file in the project root:

  • phpcs: <exclude-pattern> entries in your phpcs.xml
  • stylelint: .stylelintignore
  • eslint: .eslintignore
  • cspell: ignorePaths in .cspell.json

Automatically fix coding standard violations

ddev checks-fixes runs all three auto-fixers in sequence and prints a summary — the quickest way to clean up a module before running ddev checks:

ddev checks-fixes
ddev checks-fixes web/modules/custom/mymodule

The individual fixers can also be run directly:

ddev phpcbf web/modules/custom/mymodule
ddev stylelint --fix
ddev stylelint --fix 'web/modules/custom/mymodule/**/*.css'
ddev eslint --fix
ddev eslint --fix web/modules/custom/mymodule/js

Set up a pre-commit hook that runs phpcbf before every commit:

  1. Create .git/hooks/pre-commit if it does not already exist.
  2. Add the following:
#!/usr/bin/env bash
ddev phpcbf -q
  1. Make it executable: chmod +x .git/hooks/pre-commit.

Troubleshooting

"Error: unknown command"

Commands are only available when the DDEV project type is drupal. Confirm the type in .ddev/config.yaml:

type: drupal

Tip

Run ddev restart after editing .ddev/config.yaml.

ESLint or Stylelint reports missing plugin errors

The bundled fallback configs use globally installed plugins. If you are pointing ESLint or Stylelint at a custom config that references plugins not installed globally, either install those plugins inside the container or add them to your project's package.json and run yarn install / npm install.

PHPStan cannot find Drupal classes

Make sure drupal_root is set correctly. When using the bundled config the phpstan command sets this automatically from $DDEV_DOCROOT. If you supply your own phpstan.neon, add:

parameters:
  drupal:
    drupal_root: /var/www/html/web

Adjust the path to match your project's docroot.

Contributing

Tests are written in Bats. Install the test helper submodules first:

git submodule update --init

Then run the tests from the project root:

./tests/bats/bin/bats ./tests

Tests are triggered automatically on every push and run nightly. Contributions and bug reports are welcome.

Credits

Contributed and maintained by Martin Anderson-Clutz (@mandclu).

About

Sets up a Drupal environment for contrib code validation

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors