Skip to content

How-to guideFor module developersDrupal 11

Verify agent-generated Drupal code

An agent’s code reads as correct whether or not it is. The verification loop is the set of commands that decide the difference, and that the agent runs every iteration before it claims a task is done. Each gate catches a different class of Drupal mistake, so the order matters: cheap structural checks first, runtime checks last.

A sequence of gates the agent runs on every change, so the code you review has already passed coding standards, static analysis, the tests, and a runtime boot. The coding-standards, static-analysis, and test gates here are the ones this documentation’s own examples project runs in CI. The runtime gates run against your local site.

  • A Drupal 11 project set up for agents, per Build with AI agents: AGENTS.md in the project root and Drush allowed.
  • Drupal’s development dependencies installed with ddev composer require --dev drupal/core-dev. The package brings PHP_CodeSniffer with drupal/coder, PHPStan with mglaman/phpstan-drupal and its deprecation rules, and PHPUnit.
  • The agent allowed to run the verification commands without asking. See Let the agent use Drush.

Configure the tools once, then run the gates in this order. Run every command from the project root; the ddev prefix runs it inside the DDEV web container, where the database is reachable. An agent should not advance to a later gate until the earlier ones pass. A coding-standards failure is cheaper to read and fix than a failed test.

  1. Configure PHPStan and the test database.

    PHPStan reads its level and paths from phpstan.neon in the project root. Create it:

    phpstan.neon
    parameters:
    level: 6
    paths:
    - web/modules/custom

    Add web/themes/custom to paths once you have a custom theme. Kernel and functional tests need a database in SIMPLETEST_DB. Set it for the DDEV web container, then restart DDEV:

    Terminal window
    ddev config --web-environment-add=SIMPLETEST_DB=mysql://db:db@db/db
    ddev restart

    PHPUnit uses core’s configuration in web/core/phpunit.xml.dist, passed with -c web/core. Testing covers a project-level phpunit.xml and the other test variables.

  2. Coding standards: phpcs with Drupal and DrupalPractice.

    Terminal window
    ddev exec vendor/bin/phpcs --standard=Drupal,DrupalPractice \
    --extensions=php,module,inc,install,test,profile,theme,info,yml \
    web/modules/custom

    The --extensions list makes phpcs check .module, .install, and YAML files, which it skips by default. The DrupalPractice standard warns on a static \Drupal call in blocks, forms, controllers, field formatters and widgets, and service classes, and points at dependency injection instead. phpcs exits non-zero on warnings as well as errors, so a warning fails the gate. Run it first because it is fast and its failures are unambiguous.

  3. Static analysis: phpstan at level 6 with the Drupal extension.

    Terminal window
    ddev exec vendor/bin/phpstan analyse

    PHPStan catches what coding standards cannot: a call to a method that does not exist, a wrong argument type, a hallucinated API. With mglaman/phpstan-drupal and its deprecation rules, it also reports calls to deprecated core APIs, which is exactly where an agent’s older training shows. A class invented by the model rarely survives this gate.

  4. Tests: phpunit for the code that changed.

    Terminal window
    ddev exec vendor/bin/phpunit -c web/core web/modules/custom/your_module

    The first two gates prove the code is structured correctly and uses real APIs. The tests prove it does what you asked. Require the agent to write or update a test for the feature it built, then run it. A feature with no test is a feature the loop cannot confirm.

  5. Runtime: rebuild the cache and apply updates.

    Terminal window
    ddev drush cr && ddev drush updatedb -y && ddev drush status

    Code can pass every static gate and still fail to boot, because of a broken service definition, a bad route, or invalid YAML. Rebuilding the cache forces Drupal to recompile the container and re-collect plugins, which surfaces these. drush updatedb -y applies pending update hooks without stopping for confirmation. drush status confirms the site is still bootable.

  6. The site itself: load the feature.

    The final gate is the running site. Visit the route, place the block, or apply the recipe the agent built, and confirm it behaves. For a decoupled or front-end change, load the page in a browser. This is the only gate that catches a feature that is technically valid but wrong for a person using it.

The loop is working when a clean run of all four command gates reports no errors:

Terminal window
ddev exec vendor/bin/phpcs --standard=Drupal,DrupalPractice \
--extensions=php,module,inc,install,test,profile,theme,info,yml \
web/modules/custom \
&& ddev exec vendor/bin/phpstan analyse \
&& ddev exec vendor/bin/phpunit -c web/core web/modules/custom/your_module \
&& ddev drush cr && ddev drush updatedb -y && ddev drush status

To confirm the gates actually bite, let the agent introduce a known-bad pattern, such as a static \Drupal::service() call in a block class. Watch phpcs reject it and the agent correct it. A loop that never fails is a loop that is not checking anything.

A theme’s PHP lives in its .theme file and any classes under src/. Keep phpcs for those and the theme’s YAML, and keep PHPStan if the theme has PHP. Lint JavaScript with ESLint using core’s configuration in web/core/.eslintrc.json, since phpcs does not check JavaScript. Drop PHPUnit and make the browser the gate: confirm the component renders and the behavior attaches after an Ajax load.

When the agent works on a front end that consumes Drupal over JSON:API, the gates move to that codebase’s own toolchain. On the Drupal side, the runtime gate is a request against the API endpoint with the expected response shape.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.