How to ensure PHP code quality? Static analysis using PHPStan with Symfony
Software developers are not robots, even after seven years of PHP programming experience, I do sometimes make bugs in the code. In the age of smart IDEs, it’s getting harder and harder to make silly mistakes, most IDEs catch them very well, but it still happens.
To avoid errors in the code, we can use tools that detect them during static code analysis. What kinds of errors are detected by static code analyzers? Lots, such as calls to unknown classes/methods, number of method arguments, using undefined variables, calling methods on nullable types, types of arguments passed to methods, and much much more.
There are many tools for static PHP code analysis, but one of the most popular is PHPStan. It may be due to its ease of use, versatility, and the possibility of using many extensions, such as Symfony, doctrine, elasticsearch, monolog, guzzle, etc. I’ll show you how to easily add PHPStan to the Symfony project.
How to install PHPStan in a Symfony framework?
It is easier when we create a new environment and have a new, greenfield, clean and fresh project. Then we do not init PHPStan into the “dirty” code, but we have the code analysed from the beginning. But of course, that rarely happens. If you haven’t used static code analysis tools before, you are probably adding it because you already encounter some problems with your code.
The first step, install PHPStan via Composer
composer require --dev phpstan/phpstan
It will install PHPStan’s executable in the project bin
directory. Depending on your Symfony framework version/configuration it will be vendor/bin
or just bin
.
The second step.. just run the PHPStan analyse command (note that it is not a Symfony console command):
bin/phpstan analyse src
where src
is a directory name you want to check.
That’s it, you can start correcting bugs :). If this is a new project, you’ll probably see there are no errors… but, let’s use this in a project I was working on a while ago, and PHPstan wasn’t used in it.
How to use PHPStan in a Symfony project?
PHPStan can be run with few parameters. One of them is required, this is the directory to be checked.
The level
parameter is very important - "Specifies the rule level to run.". Analyse command can be run with one from nine levels where level 0 is the loosest and level 8 is the strictest. The default level is 0. How do these levels differ? You can check it in the documentation. If this is a new, empty project, and you are just starting to write code, it is worth using a higher level. Then you will maintain high standards from the very beginning. But if you are adding PHPStan to an existing project, it will probably be not easy to pass even the lowest level.
How does the response look like? Let’s check the project I mentioned. I will start from default level 0.
So in the src directory, PHPStan found 53 bugs. What types of errors are these?
- Access to an undefined property $foo
- Method foo() invoked with X parameter, Y required.
- Class \Bar constructor invoked with X parameters, Y required.
- Call to an undefined method bar()
- Method \Bar::foo() should return string but return statement is missing.
Even though 53 seems like a lot, it is very easy to correct it because these are simple ones.
So let’s try to raise the level, how many bugs will it find? I do not have good news:
level 2 — 629 errors
level 8 — 2695 errors
No customer will agree if you tell him, now for a few weeks I will correct all errors found by PHPStan. You need to approach it wisely.
How to configure PHPStan?
Another parameter in the analyse command is --configuration
(-c
). It's a path to file with configuration in NEON format (very similar to YAML).
What can we configure there? A lot, you can find all the options in the documentation but, for me, the most important options are paths to be checked and excluded from check, level, and ignoring errors.
When I look at the result for level 2, I can see that most of the errors are PHPDoc tag @param has invalid value ($foo): Unexpected token "$foo", expected type
. Let's be honest, this is not a critical bug, I won't be fixing all PHPDoc tags right now. Let's say I'll fix it another time, and now I'd like to ignore it (but make sure to put it on my refactoring list to keep track on the technical debt). For this, I'll use the ignore error option in my configuration file. We can use regular expressions there, so I will ignore all errors starting with "PHPDoc tag ..."
parameters:
ignoreErrors:
- '#PHPDoc tag .#'
When you add your configuration file, do not forget to add it as a parameter for the analyse command. Otherwise, PHPstan will look for a file with the default name phpstan.neon
bin/phpstan analyse -l 2 -c myPHPStanConfig.neon src
This way the number of bugs has dropped from 629 to 268. I didn’t fix these bugs, I just silenced them, I will correct them later (yeah, someday ;)) because they are not critical. I allowed myself to silence a few other minor bugs:
parameters:
ignoreErrors:
- '#PHPDoc tag .#'
- '#. typehint specified#'
- '#. return statement is missing#'
- '#Return typehint of method .#'
- '#Call to an undefined method Psr\\Container\\ContainerInterface::getParameter\(\)#'
- '#Call to an undefined method Doctrine\\ORM\\EntityRepository<.+>::find.+\(\).#'
So right now I have “only” 108 errors to check and fix. But I am still on level 2, how does it look like on level 8?
833 errors, it’s not good, but that’s better than the initial 2695 errors. Fortunately, this post is not about correcting bugs.
If this was a legacy project and it is not our priority to correct the legacy files, we can exclude whole legacy directories.
parameters:
excludePaths:
- src/Utils/*
With this configuration, PHPStan will skip all files in the src/Utils/*
directory
PHPStan extensions
By default, PHPStan does not understand the magic that Symfony and Doctrine do sometimes. We can use framework specific extensions that expand it’s knowledge.
How to install PHPStan Symfony extension?
First, we need to start with the generic PHPStan Extension Installer, this is a composer plugin that handles the automatic installation of new extensions.
Just run
composer require --dev phpstan/extension-installer
Than install PHPStan Symfony framework extension:
composer require --dev phpstan/phpstan-symfony
After that, we need to define the path to the file that describes our container in the configuration file. It’s an XML file generated in the cache/dev
directory. Extension documentation has some examples:
parameters:
symfony:
# container_xml_path: var/cache/dev/srcDevDebugProjectContainer.xml
# or with Symfony 4.2+
container_xml_path: var/cache/dev/srcApp_KernelDevDebugContainer.xml
# or with Symfony 5+
# container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
How to install PHPStan Doctrine extension?
PHPStan Doctrine extension provides support for all Doctrine magic. It checks our DQLs, QueryBuilders, validates entities and relations, recognizes magic methods like findBy*
, etc. We already have "PHPStan Extension Installer", so we just need to install extension:
composer require --dev phpstan/phpstan-doctrine
To use more advanced analysis like DQL validation we have to provide an object manager. It’s described in extension documentation. We need to create a file eg. tests/object-manager.php
use App\Kernel;
require __DIR__ . '/../config/bootstrap.php';
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
return $kernel->getContainer()->get('doctrine')->getManager();
and use it in PHPStan configuration:
parameters:
doctrine:
objectManagerLoader: tests/object-manager.php
So let’s check my project after adding these extensions.
For level 2:
For level 8:
The number of bugs found increased for level 2 from 108 to 429, and level 8 from 833 to 1166 errors. As you can see, adding extensions improves code validation efficiency.
Other PHPStan extensions
There are many other extensions, for example, you can check PHPStan strict rules extension. From their documentation:
This repository contains additional rules that revolve around strictly and strongly typed code with no loose casting for those who want additional safety in extremely defensive programming
And that’s exactly how it works. I added it to my project. The number of errors detected increased on level 2 from 429 to 684 and on level 8 from 1166 to 1459.
How to automate PHPStan in a Symfony project?
Of course, even if you start with a clean project and you have zero errors, if you had to make sure that you run PHPStan before each Commit, you would forget one time. Someone else might not use it at all and with time the number of errors would grow.
From time to time you would have to sit and fix all bugs. This is not good, the customer will not want to pay for it.
Therefore, PHPStan should be added to your CI (you probably have some?). You can add the PHPStan check run job to composer scripts or directly to the CI configuration. If it detects an error, then the build will fail, and you will know with each push that you need to correct something.
It is worth thinking about it from the beginning of the project, already at the stage of the initial setup and dockerization of the Symfony project, which I described in the previous post: Simple Docker setup for Symfony project
Conclusions
Is it worth installing PHPStan in an existing project? Of course, but.. probably not at the highest level from the beginning. You will have to start at the lowest level and work your way up gradually.
I also recommend using extensions that only increase PHPStan capabilities, and thus find more potential bugs.
If you are not coping with your code, because you have not started using PHPStan on time, you can contact us, we deal with difficult cases.
Originally published at https://accesto.com.