mglaman/phpstan-drupal
PHPStan extension for Drupal that teaches static analysis about Drupal's APIs—hooks, services, plugins, entity types, and test base classes—so it catches real bugs and deprecations in modules, themes, and profiles while avoiding false positives.
Install the extension and PHPStan as dev dependencies:
composer require --dev mglaman/phpstan-drupal phpstan/phpstan
If you also want Drupal deprecation reporting, add the deprecation rules:
composer require --dev phpstan/phpstan-deprecation-rules
Create or update phpstan.neon at the project root:
includes:
- vendor/mglaman/phpstan-drupal/extension.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
parameters:
level: 1
drupal_root: web
paths:
- web/modules/custom
drupal_root must point at the directory containing core/ and sites/, typically web in a Composer-based Drupal project. Run analysis with:
vendor/bin/phpstan analyse --memory-limit=1G
Look first at vendor/mglaman/phpstan-drupal/extension.neon to see exactly which rules and dynamic return-type extensions your installed version registers. The README’s drupal_root and configuration sections are the next stop.
A good first use case is running the extension against a single custom module at level 1. It immediately catches common Drupal mistakes: calls to removed or deprecated APIs, incorrect assumptions about \Drupal::service() return types, malformed plugin annotations, and hook implementations whose signatures no longer match the documented hook.
Ratchet from a baseline. On an existing codebase, generate a baseline once and commit it:
vendor/bin/phpstan analyse --generate-baseline
Then add the baseline to phpstan.neon:
includes:
- vendor/mglaman/phpstan-drupal/extension.neon
- phpstan-baseline.neon
New code is held to the configured level while legacy noise stays suppressed. Periodically regenerate or shrink the baseline rather than letting it grow.
Use it in CI as a gate. A typical CI step:
vendor/bin/phpstan analyse \
--no-progress \
--error-format=github \
--memory-limit=1G
Keep the Drupal root, Composer autoloader, and PHPStan config in the same checkout. Running from the project root avoids autoload and relative-path surprises.
Analyze one module while developing. Point paths at the module you are changing, then widen to all custom code for CI:
parameters:
paths:
- web/modules/custom/my_module
This keeps local feedback fast and avoids unrelated legacy errors.
Provide a bootstrap file for constants and globals. Drupal code often expects constants such as DRUPAL_ROOT. Use bootstrapFiles for lightweight definitions, not to manually boot the kernel:
parameters:
bootstrapFiles:
- phpstan-bootstrap.php
<?php
define('DRUPAL_ROOT', __DIR__ . '/web');
Keep the bootstrap silent. Any output can corrupt JSON or CI annotations.
Register custom entity types. Dynamic return types for entity storage and load() depend on knowing the entity class. For custom entities, add entityMapping:
parameters:
entityMapping:
my_entity:
class: Drupal\my_module\Entity\MyEntity
storage: Drupal\my_module\MyEntityStorage
Without this, \Drupal::entityTypeManager()->getStorage('my_entity')->load($id) may degrade to a generic object type.
Add project-specific rules. The extension gives you Drupal-aware infrastructure; custom architectural rules are inexpensive:
services:
-
class: App\PHPStan\Rules\NoDirectSuperglobalRule
tags: [phpstan.rules.rule]
Use custom rules for team conventions such as forbidding \Drupal::service() in classes that should use constructor injection, or requiring accessCheck() on entity queries.
Use the deprecation workflow during upgrades. For Drupal major-version upgrades, include phpstan/phpstan-deprecation-rules and run analysis across custom modules. Treat deprecation errors as a migration queue. Filter by deprecation message or identifier in CI so you can track progress without failing every build immediately.
Wrap common commands in Composer scripts.
{
"scripts": {
"phpstan": "phpstan analyse --memory-limit=1G",
"phpstan:baseline": "phpstan analyse --generate-baseline --memory-limit=1G"
}
}
This keeps local and CI invocations aligned.
drupal_root is relative to the config file, not the shell’s current directory. If PHPStan reports that it cannot determine the Drupal root, check this first.drupal_root at web/core. It must be the Drupal docroot containing core/ and sites/.sites/default/settings.php; missing database credentials are a common cause of bootstrap failures.\Drupal::service('my_module.foo') resolves only when the service definition is discoverable. If it returns object, confirm the module is enabled or present in the analyzed codebase, or add a local @var annotation.@var or @return annotations where PHPStan still reports mixed values..module, .install, and .theme files are analyzed. The extension generally registers Drupal file extensions, but if files are skipped, inspect fileExtensions in your config and add the Drupal extensions explicitly.treatPhpDocTypesAsCertain: false only if weak Drupal annotations are causing clear false positives.@phpstan-ignore with a rule identifier or ignoreErrors with identifier over broad path-based ignores. Avoid setting reportUnmatchedIgnoredErrors: false permanently; it hides stale suppressions.vendor/bin/phpstan clear-result-cache
--memory-limit, reduce parallel.maximumNumberOfProcesses, or run with --debug locally to get a clearer stack trace. --debug disables parallelism.excludePaths for generated PHP, compiled Twig, and vendored contrib you do not own. Analyze custom code, not the whole docroot.mglaman/phpstan-drupal tracks PHPStan and Drupal major versions; pin both in composer.json and upgrade them together.How can I help you explore Laravel packages today?