Drupal 9: Running PHPStan On Drupal Custom Modules

PHPStan is a great command line tool for looking at how your PHP code will run without actually running it. It's great for finding potential bugs that you wouldn't have otherwise discovered using other tools or through unit testing.

With regards to Drupal projects there is a little problem in that PHPStan doesn't know how to interpret Drupal plugins, entities, controllers or all the other Drupal architecture that goes into a Drupal module. For this reason, if you try to run PHPStan on your module code you'll find that it produces a lot of errors regarding missing objects or incorrect parameters.

Thankfully, it's possible to easily teach PHPStan about Drupal and make the tool more useful when writing Drupal code. First we need to install it.

Install PHPStan In A Drupal Site

To install PHPStan into a Drupal site you need to use composer and include the following pages.

composer require  --dev phpstan/phpstan phpstan/extension-installer mglaman/phpstan-drupal phpstan/phpstan-deprecation-rules

The packages we are installing are:

  • phpstan/phpstan - The core PHPStan package.
  • phpstan/extension-installer - This is a composer plugin for automatically installing PHPStan extensions which means we can load packages into the PHPStan runtime.
  • mglaman/phpstan-drupal - PHPStan Drupal is a package created by Matt Glaman that allows PHPStan to understand the Drupal object structure.
  • phpstan/phpstan-deprecation-rules - A PHPStan package that will print out deprecation messages, which helps us detect deprecations in Drupal code.

Next, we need to create a configuration file so that PHPStan knows what level to run at and what paths we are interested in. Create a file called phpstan.neon in the root of your project and add the following configuration to it.

parameters:
    level: 0
    paths:
        - web/modules/custom

PHPStan has a number of levels that dictate what sort of things it will look for. Level 0, being the lowest level, looks for some basic checks like variables not being assigned and unknown classes being used. You can find the full description of the different levels on the PHPStan website, but I'll also include them here for completeness.

  • 0 - basic checks, unknown classes, unknown functions, unknown methods called on $this, wrong number of arguments passed to those methods and functions, always undefined variables
  • 1 - possibly undefined variables, unknown magic methods and properties on classes with __call and __get
  • 2 - unknown methods checked on all expressions (not just $this), validating PHPDocs
  • 3 - return types, types assigned to properties
  • 4 - basic dead code checking - always false instanceof and other type checks, dead else branches, unreachable code after return; etc.
  • 5 - checking types of arguments passed to methods and functions
  • 6 - report missing typehints
  • 7 - report partially wrong union types - if you call a method that only exists on some types in a union type, level 7 starts to report that; other possibly incorrect situations
  • 8 - report calling methods and accessing properties on nullable types
  • 9 - be strict about the mixed type - the only allowed operation you can do with it is to pass it to another mixed

Note that using rule level 5 means that you include rule levels 0, 1, 2, 3, 4 and 5 in the analysis. 

My advice, especially when running this on pre-existing codebases, is to start at level 0 and work your way up to something like level 5. If you start at level 9 you are bound to see lots of errors being produced, even in seemingly well written code. You should work with your team to decide on what level you should work at and what to do with errors that get produced from the analysis.

Running PHPStan

With all that in place we can now run PHPStan using the following command.

./vendor/bin/phpstan

If you are having problems with memory you can pass in the --memory-limit flag to increase the amount of memory available to the tool. This argument takes the normal PHP memory limit values so you can use 1G or 1000M to set the value to 1 gigabyte.

./vendor/bin/phpstan --memory-limit=1G

This will produce output that looks a little like the following (depending on what you are testing).

./vendor/bin/phpstan
Note: Using configuration file phpstan.neon.
 12/12 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ----------------------------------
  Line   mymodule.module
 ------ ----------------------------------
  37     Undefined variable: $currentUser
 ------ ----------------------------------

 ------ ------------------------------------------------------------------------------
  Line   src/Form/CreateEntityForm.php
 ------ ------------------------------------------------------------------------------
  28     \Drupal calls should be avoided in classes, use dependency injection instead
 ------ ------------------------------------------------------------------------------

By default, PHPStan will output the errors as a table. You can change this to a variety of different formats using the --error-format flag. For example, we can change the output to json and write that output to a file like this.

./vendor/bin/phpstan --error-format=json --no-progress --ansi > phpstan_analysis.json

This allows us to pick up and analyse the errors using upstream systems.

You can see the available error formats on the PHPStan documentation page.

Side note, you can also get PHPStan to open links in your editor of choice by adding options to your phpstan.neon file. In this example, I'm allowing the output of my PHPStan command to open in PHPStorm.

parameters:
    level: 0
    paths:
        - web/modules/custom
    editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'

This means that I just need to click on the link in my terminal to open up the file and go to the line in PHPStorm. That is awesome!

A Common Problem When Analysing Drupal Code

When running analysis on Drupal code I found that the following error was produced a lot.

 ------ -------------------------------------------------------------------------------------------------
  x      Unsafe usage of new static().
         💡 See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static
 ------ -------------------------------------------------------------------------------------------------

This is caused by the following construct.

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition
    );
  }

This is taken from a Drupal plugin, but the same sort of thing can be seen in controllers, forms, and pretty much anything that uses the dependency injection system in Drupal. The reason that PHPStan is throwing this error is because if the class is extended and the constructor is overridden with different parameters then this might cause breakages.

There's some information on why this use of new static() is unsafe on the PHPStan website.

Whilst this is true, this is also how Drupal functions and there are good extensibility reasons as to why Drupal works like this. The only option, therefore, is to suppress this error message by setting the ignoreErrors option in the phpstan.neon configuration file.

parameters:
    level: 0
    paths:
        - web/modules/custom/mymodule
    ignoreErrors:
        - '#Unsafe usage of new static\(\)#'

Adding this option turns off that error and means we can focus on other things that are important to improving the code quality of the custom code.

Conclusion

PHPStan is a great tool, and it should be a part of your development workflow. As I said before, it's worth deciding before hand on the rule level that you are working towards as setting a high level can lead to spending a long time refactoring your codebase to solve a potential issue in your code. There's a careful balance to be made between the rule level, your budget and the overall quality of the codebase.

Massive thanks to Matt Glaman for creating the phpstan-drupal extension.

Comments

Nice quick easy how to. Thanks!

My little slightly OT thing that's always made me wonder:-

there are good extensibility reasons as to why Drupal works like this

But what are the reasons? Admittedly for plenty of cases the parent can be trusted not to change as it's an API. But for any cases it's as easy to:

$plugin = new static (...);
$plugin->var = $container->get('service');
return $plugin;

or better yet add getService() setService() methods and call $plugin->setService($container->get('service)); HT Thomas (Drunken Money) plugin code for Search API for that pattern.

I think the move to having traits to add services in Drupal seems to also be a move toward doing something easier to extend.

Permalink

@ekes,

Now that is a good question! When I was doing research for this post I did look into why this was the case, but I only found that there were reasons. Nothing specific I'm afraid.

I'm thinking that it allows maximum flexibility for whatever you need to do in Drupal and can be used in forms, controllers, plugins or where ever you need it.

It did get me wondering as well. I have written and talked a lot about dependency injection in Drupal 8+, and even about this structure, but I haven't really looked into why we use this particular pattern.

I'll take a look at the Search API module to see how they do it.

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
8 + 9 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.