I recently got a new opportunity to work with a company as a developer. One of my first tasks is to implement PHP Static Analysis on an 8 year old-ish codebase. The task seemed daunting at first but I had 2 things going for me; first, they were using Laravel framework so we can easily pull up Larastan, and second, the codebase has good amount of tests which gave me a huge boost in confidence.
In this blog, I aim to give you few tips when implementing static analysis on a legacy codebase, and explain to you some of the static analysis errors that I encountered and how I solved it.
First Time
If this is your first time implementing static analysis, I would suggest reading through the PHPStan Documentation first before even installing it on your codebase. Their documentation is quite comprehensive so it should not take you very long. I would also suggest, though not necessary, to watch Nuno Maduro's talk about Types in PHP for better understanding of PHP types and Larastan.
By now you should know that there are rule levels in PHPStan - from Level 0 to Level 9. Levels determine the strictness of the rules where 0 is the loosest and 9 is the strictest. If you're implementing it on a huge codebase you should always start at Level 0 or you will get overwhelmed with too many errors to fix.
Installation
Installing Larastan should be quick and easy, just follow through the documentation on the readme file in the repository. By now, you should have a configuration file within the root of your project directory. Mine is stored as phpstan.neon
and looks more or less like this:
includes:
- ./vendor/nunomaduro/larastan/extension.neon
parameters:
checkMissingIterableValueType: false
noUnnecessaryCollectionCall: false
reportUnmatchedIgnoredErrors: false
# Paths to scan and analyse.
paths:
- app
# The level: 9 is the highest level.
level: 0
# Circle CI configuration.
parallel:
jobSize: 20
maximumNumberOfProcesses: 8
# List of errors to be ignored.
ignoreErrors:
- '#PHPDoc tag @var#'
- '#Unsafe usage of new static#'
# List of paths that are excluded.
excludePaths:
- tests/
Once you've created the configuration file, just run ./vendor/bin/phpstan analyse
, wait for couple of seconds (might take longer depending on the size of your project) and it should show you the errors on your terminal.
Let's go through some of the errors that I have encountered.
Error Patterns
Access to an undefined property App\Foobar::$baz.
This error is self-explanatory. Within the class Foobar you are doing a $this->baz
property call. However the property is not actually declared in the class. Although PHP allows this through dynamic property, static analysis is protecting you from making unexpected property calls to an object.
Fix: Just declare the property on the class or remove it entirely.
Method App\Foobar::handle() should return int but return statement is missing.
Another self-explanatory error. The method handle()
within class Foobar is expected to return something but there is no return statement. This is usually because there is a doc block above the method declaration.
/*
* @return int
*/
public function handle()
{
// Some code without return statement...
}
Fix: Add a return statement or remove the doc block.
Relation 'user' is not found in App\Models\Post model.
This error was a bit tricky at first, because it would still appear even though the relation user
has been declared on the App\Models\Post
model class.
Fix: Add a return type on the relation method.
// In App\Models\Post class...
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function user() : BelongsTo // Add this..
{
return $this->belongsTo(User::class);
}
Deprecated in PHP 8.0: Required parameter $foo follows optional parameter $bar.
Another self-explanatory error and is obviously an issue only on PHP 8.0 and up. Basically, what's happening is that there is a method within your class that looks something like this.
public function something($bar = null, int $foo)
{
// ...
}
As you can see, $bar
is an optional parameter while $foo
is not. Required parameters should be at the left of optional parameters.
Fix: Refactor the method and its usages to make sure required parameters are on the left.
app/Console/Commands/Foo.php: Result of method Illuminate\Console\Command::error() (void) is used.
This is because in the handle
method of the Foo.php
class, we have this call
public function handle()
{
// Some codes...
return $this->error('...');
}
The issue is that the result of $this->error()
call is void type and therefore should not be used as a return statement.
Fix: Update return statement with the correct integer code.
And that's pretty much all of the error patterns I have encountered implementing level 0 static analysis. On the first run, there were 380 errors found with these patterns on different parts of the codebase.
Hopefully you are starting to realize the benefits and protection static analysis can give you at the very beginning.
I will continue this article once I get to work on the next levels. Cheers!