 [ ← blog / drupal ](/) Jan 08, 2019  ·  \#drupal  ·  6 min read 

# Writing better Drupal code with static analysis using PHPStan



PHP is a loosely typed interpreted language. That means we cannot compile our scripts and find possible execution errors without doing explicit inspections of our code. It also means we need to rely on conditional type checking or using [phpDoc comments](http://docs.phpdoc.org/guides/types.html) to tell other devs or IDE what kind of value to expect. Really there is no way to assess the quality of the code or discover possible bugs without thorough test coverage and regular review.

If you use PhpStorm, you will notice all of their helpers which analyze your code and add static analysis. Such as "Watch out, there's a good chance you did not catch this exception!" PhpStorm reads the phpDoc annotations to help provide type checking as well. This gets kicked up to level 100 using the [Php Inspections (EA/SA) plugin](https://github.com/kalessil/phpinspectionsea).

     ![](/sites/default/files/styles/large/public/Screen%20Shot%202019-01-08%20at%201.26.05%20PM.png?itok=MqXYKc9Z) 

 

That's awesome. It's pretty amazing that PhpStorm and a few plugins can give us some stability in our PHP code that allows acting like we *might* be working in a compiled language (okay, that's a stretch, but the point is there.)

There are a few problems with this approach, though

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.

But, wait! What about [PHP\_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)? PHPCS gives us some benefits, but it is not a full static analysis tool that would help us find bugs in our software. When running PHPCS, individual files are tokenized and then parsed. This allows for a line-by-line analysis of an individual file. It does not, however, let you check if the class you referenced actually exists or not. Or that your method expects **MySpecificObjectInterface** but in reality, several calls will pass  **SomeOtherObjectInterface**.

There are a [quite few static analysis tools out there](https://github.com/exakat/php-static-analysis-tools), but none will work with Drupal out of the box. Why? Because Drupal has a magical autoloading system that does not get dumped into Composer's autoloader. The tool needed to extendable. And that's how I found [PHPStan](https://github.com/phpstan/phpstan). What I really like about PHPStan is that it is able to inspect your entire codebase and find out if a class does not exist, if it is called incorrectly, like you actually compiled your PHP project and didn't even run it.

So, what happens when you try to run PHPStan and analyze a Drupal module (or core) out of the box?

     ![](/sites/default/files/styles/large/public/Screen%20Shot%202019-01-08%20at%206.24.17%20PM.png?itok=NDjtEWMo) 

 PHPStan executed against the Address module, without the Drupal extension

Nothing. Because PHPStan has no idea how to load any of the files. PHPStan relies on being able to load classes or functions through Composer's generated autoload information. When you enable a module or a theme, none of the information on how to load its files is added to the Composer autoload information. All of that data is set up when Drupal's container is built.

## A Drupal extension for PHPStan

I would like to introduce [**phpstan-drupal**](https://github.com/mglaman/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](https://www.drupal.org/project/drupal/issues/3020142).

     ![](/sites/default/files/styles/large/public/Screen%20Shot%202019-01-08%20at%209.54.14%20PM.png?itok=wArmrGVi) 

 PHPStan run against the state\_machine module, with the Drupal extension

To get started, you need to add **mglaman/phpstan-drupal** as a developer dependency.

```bash
composer require mglaman/phpstan-drupal --dev
```

Then create a **phpstan.neon** file. Here's an example that I am using

```bash
parameters:
	# Ignore tests
	excludes_analyse:
		- *Test.php
		- *TestBase.php
	# PHPStan Level 1
	level: 1
includes:
	# Add the phpstan-drupal extension
	- vendor/mglaman/phpstan-drupal/extension.neon

```

Now, let's dive into some more details

### 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.

The extension supports discovering Drupal in the following scenarios vanilla Drupal project setup and Composer project template setups with either the **web** or **docroot** directory.

The first task was to make copies of the extension discovery classes. Since Drupal core has a dependency on Symfony 3 it could not be added as a developer dependency -- PHPStan uses Symfony 4's Console component.

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

There's a lot to walk through, but the source is here: [https://github.com/mglaman/phpstan-drupal/blob/master/src/Drupal/Bootst…](https://github.com/mglaman/phpstan-drupal/blob/master/src/Drupal/Bootstrap.php#L104). This loads legacy include files, adds namespaces, ensures extension files can be loaded, and **hook\_hook\_info** files.

### 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](https://plugins.jetbrains.com/plugin/7487-drupal-symfony-bridge) to provide typing from the services container. For example, *entity\_type.manager* would knowingly be *EntityTypeManagerInterface*.

The extension loads all available **services.yml** from core and modules. This is then parsed and put into a ServicesMap. I borrowed the concepts from the [PHPStan Symfony extension](https://github.com/phpstan/phpstan-symfony). However, the Symfony container gets dumped and is easier to load and parse, not nearly as dynamic as Drupal

```php
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;
  }
}
```

The PHPStan Drupal extension implements a DynamicMethodReturnTypeExtension rule that will return the proper class object type based on the requested service.

There are some gotchas. It does not work well for services which define **calls**, **factory**, and **configurator**.

### 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

```bash
drupal:
	entityTypeStorageMapping:
		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!

- I would like to add test coverage (started <https://github.com/mglaman/phpstan-drupal/pull/10>).
- I need to better document the rules that it comes with

 

  ![](/themes/custom/mglaman/images/headshot.webp)Thanks for reading

I write about Drupal, PHP, and the quiet infrastructure behind large sites — caching, config, and testing. New posts land every week or two.

 [Subscribe via RSS →](/rss.xml) [About me →](/about-me) 

 



 

  [← Older

Two Zero One Nine

 ](/blog/two-zero-one-nine) [Newer →

Come to MidCamp and kick off contribution sprints for DrupalCon Seattle

 ](/blog/come-midcamp-and-kick-contribution-sprints-drupalcon-seattle)