Writing better Drupal code with static analysis using PHPStan

  1. Writing custom inspections, like for Drupal, requires learning Java and building a PhpStorm plugin.
  2. Everyone on the development team needs to have PhpStorm
  3. You can’t execute this over a CI process and make it codified.
PHPStan executed against the Address module, without the Drupal extension

A Drupal extension for PHPStan

I would like to introduce phpstan-drupal. I spent two weeks in December working on this extension so that I could analyze Drupal Commerce, our dependencies, and Drupal core itself. It was definitely a fun challenge and even uncovered a bug in Drupal core due to a duplicate function name in a test module.

PHPStan run against the state_machine module, with the Drupal extension
composer require mglaman/phpstan-drupal --dev
# Ignore tests
- *Test.php
- *TestBase.php
# PHPStan Level 1
level: 1
# Add the phpstan-drupal extension
- vendor/mglaman/phpstan-drupal/extension.neon

Bootstrapping Drupal’s autoloading and namespaces without a database

Everything about Drupal’s bootstrap and container requires the database. At first, I had tried initializing DrupalKernel and only touching methods which did not reach into the database (or try to at least mock it.) That was a big failure. I wish I had started writing this blog as I went down those rabbit holes.

$this->extensionDiscovery = new ExtensionDiscovery($this->drupalRoot);
$profiles = $this->extensionDiscovery->scan('profile');
$profile_directories = array_map(function ($profile) {
return $profile->getPath();
}, $profiles);
$this->moduleData = $this->extensionDiscovery->scan('module');
$this->themeData = $this->extensionDiscovery->scan('theme');

Return typing from the service container

When you fetch a service from the container nothing defines what should be returned. In PhpStorm, you can use the Drupal Symfony Bridge to provide typing from the services container. For example, entity_type.manager would knowingly be EntityTypeManagerInterface.

foreach ($extensionDiscovery->scan('module') as $extension) {
$module_dir = $this->drupalRoot . '/' . $extension->getPath();
$moduleName = $extension->getName();
$servicesFileName = $module_dir . '/' . $moduleName . '.services.yml';
if (file_exists($servicesFileName)) {
$serviceYamls[$moduleName] = $servicesFileName;
$camelized = $this->camelize($extension->getName());
$name = "{$camelized}ServiceProvider";
$class = "Drupal\\{$moduleName}\\{$name}";

if (class_exists($class)) {
$serviceClassProviders[$moduleName] = $class;

Dynamic return typing for entity storage from the entity type manager

The extension also provides a DynamicMethodReturnTypeExtension for the entity type manager service. Since modules have custom entities and custom storages, this is something which can be configured in your phpstan.neon file. The extension provides some defaults

node: Drupal\node\NodeStorage
taxonomy_term: Drupal\taxonomy\TermStorage
user: Drupal\user\UserStorage

What else does it have?

  • GlobalDrupalDependencyInjectionRule: don’t call \Drupal::service when dependency injection is possible (performance, code standards)
  • DiscouragedFunctionsRule: copied from phpcs
  • PluginManagerSetsCacheBackendRule: catch a plugin manager which does not set a cache backend for its definitions (performance.)
  • EnhancedRequireParentConstructCallRule: improves handling of empty parent constructor calls. YAML plugin managers do not call their instructor. Need to abstract our some other plugin manager assertions.

What’s next?

I have no idea!



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Matt Glaman

Matt Glaman

Open source developer, working with Drupal and building Drupal Commerce.