PHP is a dynamic language and while this certainly has its benefits, it also means that it’s not uncommon to see errors about calling an undefined method, or an invalid argument count in the logs. What’s even worse is that the application will simply crash when these types of mistakes occur, resulting in a bad user experience and frustrated customers.

The solution to this problem is static analysis. For languages such as Java and C# this isn’t something new, it is a built-in feature that gets performed whenever the code is compiled.

Recently various tools have popped up that allow running static analysis on PHP code. To name a few: Psalm, Phan and PHPStan. The goal of these tools is to reduce the amount of errors before the first test is written.

In this blog post I want to focus more on the latter since it’s what we have been using the most at madewithlove.

Getting started with static analysis

The tool can be installed into any project through Composer.

composer require --dev phpstan/phpstan

PHPStan requires PHP 7.1 to be installed on the environment in order to run. But the code it needs to analyze can be written for PHP 5.6. Having properly typed code through either type hints or docblocks will lead to better results.

Once installed, the binary can be used to analyze a directory and output any mistakes encountered.

 vendor/bin/phpstan analyze src
------ -------------------------------------------------------------------------
  Line   Abstracts/AbstractIndex.php
 ------ -------------------------------------------------------------------------
  66     Method ElasticSearcher\Abstracts\AbstractIndex::setTypes() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  74     Method ElasticSearcher\Abstracts\AbstractIndex::getTypes() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  84     Method ElasticSearcher\Abstracts\AbstractIndex::setSettings() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  92     Method ElasticSearcher\Abstracts\AbstractIndex::getSettings() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  102    Method ElasticSearcher\Abstracts\AbstractIndex::getType() should return
         array but returns $this(ElasticSearcher\Abstracts\AbstractIndex).
 ------ -------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------
  Line   Abstracts/AbstractQuery.php
 ------ ---------------------------------------------------------------------
  93     PHPDoc tag @param has invalid value (null|string): Unexpected token
         "\n\t ", expected TOKEN_VARIABLE at offset 48
 ------ ---------------------------------------------------------------------

 ------ ---------------------------------------------
  Line   ElasticSearcher.php
 ------ ---------------------------------------------
  75     Negated boolean expression is always false.
  87     Negated boolean expression is always false.
 ------ ---------------------------------------------

 ------ -------------------------------------------------------------------
  Line   Managers/DocumentsManager.php
 ------ -------------------------------------------------------------------
  132    Method ElasticSearcher\Managers\DocumentsManager::exists() should
         return bool but returns array|bool.
 ------ -------------------------------------------------------------------

 [ERROR] Found 9 errors

I analyzed our popular Elasticsearcher package and got the following result.

PHPStan has different rule levels, each of them more strict than the one before. By default it will use level 0 which check for “obvious” mistakes such as extra arguments being passed or syntax errors. This level is a good place to start for legacy codebases and over time the level can be increased for more stricter analysis. There is also a  max level that is aimed at static purists, which will always run the highest level available.

It’s possible that PHPStan outputs certain errors that are not really errors but the result of magic behavior added by a third-party package or framework. These errors can be fixed by installing the relevant extension through Composer and configuring it in a phpstan.neon file.

includes:
    - ../vendor/phpstan/phpstan-doctrine/extension.neon

The phpstan.neon file also allows configuring a lot of different parameters. One very common setting is the ignoreErrors parameter. This allows to ignore errors based on regular expressions. Let’s say that we want to ignore the Negated boolean expression is always false. error from our previous run, we can do so by adding the following:

parameters:
    ignoreErrors:
       - '#Negated boolean expression is always false.#'

If we ever do end up fixing this reported error and it no longer occurs PHPStan will warn us that the ignored error has become obsolete.

------ -------------------------------------------------------------------------
  Line   Abstracts/AbstractIndex.php
 ------ -------------------------------------------------------------------------
  66     Method ElasticSearcher\Abstracts\AbstractIndex::setTypes() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  74     Method ElasticSearcher\Abstracts\AbstractIndex::getTypes() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  84     Method ElasticSearcher\Abstracts\AbstractIndex::setSettings() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  92     Method ElasticSearcher\Abstracts\AbstractIndex::getSettings() should
         return array but returns
         $this(ElasticSearcher\Abstracts\AbstractIndex).
  102    Method ElasticSearcher\Abstracts\AbstractIndex::getType() should return
         array but returns $this(ElasticSearcher\Abstracts\AbstractIndex).
 ------ -------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------
  Line   Abstracts/AbstractQuery.php
 ------ ---------------------------------------------------------------------
  93     PHPDoc tag @param has invalid value (null|string): Unexpected token
         "\n\t ", expected TOKEN_VARIABLE at offset 48
 ------ ---------------------------------------------------------------------

 ------ -------------------------------------------------------------------
  Line   Managers/DocumentsManager.php
 ------ -------------------------------------------------------------------
  132    Method ElasticSearcher\Managers\DocumentsManager::exists() should
         return bool but returns array|bool.
 ------ -------------------------------------------------------------------

 ---------------------------------------------------------------------------------------------------------
  Error
 ---------------------------------------------------------------------------------------------------------
  Ignored error pattern #Negated boolean expression is always false.# was not matched in reported errors.
 ---------------------------------------------------------------------------------------------------------

 [ERROR] Found 8 errors

 Be sure to read through the full documentation to see what’s possible.

On a lot of our projects, PHPStan is included as part of the CI pipeline. Doing this means that when developers are writing and reviewing code they can focus more on the core functionalities instead of worrying about syntax and type errors making it to production.