Creating Custom PHP Stan Rules For Your Codebase
Static Analysis with PHP Stan
One of the most helpful things you can do for codebase is introduce static analysis tools. These are tools that look at your code not to determine runtime errors, but to check if it follows certain syntactical rules. Psalm and PHP Stan are the main choices for most Laravel/PHP applications, but you may also recognize Eslint, Rust-Analyzer, and even Typescript itself. First, let’s see why this could be useful.
An Example in Code
We recently ran into this bug in our software when we reset a user’s subscription information after their automatic renewal:
It looks fine at first glance, and unfortunately wasn’t caught in the tests that were written since it runs successfully. However, there’s a nasty bug here.
Carbon is a common datetime package for PHP/Laravel projects, but has some quirks—one of which is that it is mutable by default. This means that our first call to Carbon::now()
has accidentally been changed by the second line instead of returning a new copy of the date.
Carbon does have a way we can avoid this, which is just making sure that we use a CarbonImmutable
class instead.
Okay, great! We’ve fixed the problem. But how can prevent this in the future 🤔?
Creating Custom PHP Stan Rules
The strategy we decided to take, is to always use the CarbonImmutable
class instead of the base Carbon
class. PHP Stan is extendable in that it lets us create custom rules, provided that we know how to write them. Let’s make a custom rule that will allow us to check that we only use the right class.
Writing a Test File
First things first, let’s decide what we need to test. We’ll create a new file with a class that’s violating this rule. Here, we import Carbon to use as the return value:
We need to set up a couple more files as well. We need the actual test itself, and we need to update the PHP Stan config file. Let’s create our new rule first:
and update our Stan config:
Analyzing the Syntax
Great, now let’s start checking the file. We can run the following command to analyze only our Foo class, which will dump all of the node information from our syntax parser.
vendor/bin/phpstan analyse -l 8 --debug /tests/phpstan/no-carbon-imports.php
This will output the names of the file’s nodes and their types. Essentially, these static analysis tools focus on the structure of your code, using what’s called an Abstract Syntax Tree. If you’ve written a compiler or transplier before, you’ll recognize this concept. Ours should be the first line or two of our test file output, and we’ll quickly see the node of our AST we want to use, a PhpParser\Node\Stmt\UseUse::class
. Let’s add that to our rule:
Depending on what we need to check, we might have to dig deeper into the node structure to find what we are looking for. For something like checking the return types of functions, it would be more complicated. In our case, we just want to get the name of this node and then look at each section of the name. Let’s dump that and take a look:
Creating Our Rule
Now we need to write our rule. We want to check if we have an import from Carbon
that is not using CarbonImmutable
. If we have no errors, we return an empty array. If we do have errors, we need to return a RuleErrorBuilder
which is part of PHP Stan.
Here’s what our finished rule looks like:
Updating Our Baseline
All that’s left to do is update our baseline file. We’ll run the following:
vendor/bin/phpstan analyse clear-result-cache && vendor/bin/phpstan analyse --generate-baseline
This will create a new phpstan-baseline.neon
file in our repo so that we can prevent the issue now and have a backlog of places to solve this in the future!
Conclusion
Using static analysis tools to create these custom rules can be a fantastic way to clean up your codebase and make sure things aren’t missed in code review. If you’re interested in learning more about Abstract Syntax Trees or PHP Stan rules, check out their documentation to see a bit more behind the scenes.
I also recommend spending some time writing a compiler. I recently completed one in Rust if you’d like to check it out! It taught me a great deal about AST parsing and understanding how code syntax takes its shape.