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.
Prerequisites
Section titled “Prerequisites”- A Drupal 11 project set up for agents, per Build with AI agents:
AGENTS.mdin the project root and Drush allowed. - Drupal’s development dependencies installed with
ddev composer require --dev drupal/core-dev. The package brings PHP_CodeSniffer withdrupal/coder, PHPStan withmglaman/phpstan-drupaland 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.
-
Configure PHPStan and the test database.
PHPStan reads its level and paths from
phpstan.neonin the project root. Create it:phpstan.neon parameters:level: 6paths:- web/modules/customAdd
web/themes/customtopathsonce you have a custom theme. Kernel and functional tests need a database inSIMPLETEST_DB. Set it for the DDEV web container, then restart DDEV:Terminal window ddev config --web-environment-add=SIMPLETEST_DB=mysql://db:db@db/dbddev restartPHPUnit uses core’s configuration in
web/core/phpunit.xml.dist, passed with-c web/core. Testing covers a project-levelphpunit.xmland the other test variables. -
Coding standards:
phpcswith 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/customThe
--extensionslist makesphpcscheck.module,.install, and YAML files, which it skips by default. TheDrupalPracticestandard warns on a static\Drupalcall in blocks, forms, controllers, field formatters and widgets, and service classes, and points at dependency injection instead.phpcsexits 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. -
Static analysis:
phpstanat level 6 with the Drupal extension.Terminal window ddev exec vendor/bin/phpstan analysePHPStan catches what coding standards cannot: a call to a method that does not exist, a wrong argument type, a hallucinated API. With
mglaman/phpstan-drupaland 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. -
Tests:
phpunitfor the code that changed.Terminal window ddev exec vendor/bin/phpunit -c web/core web/modules/custom/your_moduleThe 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.
-
Runtime: rebuild the cache and apply updates.
Terminal window ddev drush cr && ddev drush updatedb -y && ddev drush statusCode 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 -yapplies pending update hooks without stopping for confirmation.drush statusconfirms the site is still bootable. -
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.
Verify
Section titled “Verify”The loop is working when a clean run of all four command gates reports no errors:
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 statusTo 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.
Variations
Section titled “Variations”Themes and front-end code
Section titled “Themes and front-end code”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.
Decoupled and API consumers
Section titled “Decoupled and API consumers”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.
Related
Section titled “Related”- Build with AI agents, which gives the agent permission to run these commands.
- Where AI agents get Drupal wrong, which maps each failure class to the gate above that catches it.
- Review what the checks miss, for the mistakes no command can catch.
- Testing, the full picture of PHPUnit in Drupal.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.