Ensuring clean, maintainable code is vital in any Laravel project. While PHPStan is an excellent static analysis tool for identifying code issues, the raw JSON output it generates isnβt always human-friendly. Thatβs where a custom reporting script comes in handy.
In this post, Iβll walk you through a Bash script I use in my Laravel projects to parse and beautify PHPStan output. This script splits issues by identifier and generates a neat summary for quick analysis.
π¦ Prerequisites
Before using the script, make sure you have PHPStan and jq
installed.
Install PHPStan
You can install PHPStan via Composer:
composer require --dev phpstan/phpstan larastan/larastan
Create PHPStan config file at the root directory:
touch phpstan.neon.dist
Then paste the following configuration:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
treatPhpDocTypesAsCertain: false
paths:
- app/
- config/
- database/
- routes/
- support/
level: 4
excludePaths:
- bin/
- node_modules/
- stubs/
- vendor/
- tests/
- resources/
At the moment, I use Level 4 to 6, depends on the project needs. You can read more about the Rule Levels here.
Install jq
jq
is a lightweight JSON processor required to parse PHPStan's JSON output.
macOS
brew install jq
Ubuntu/Debian
sudo apt-get install jq
Windows
Use Chocolatey or download manually from https://stedolan.github.io/jq/download/
π Recommended Directory Structure
project-root/
βββ .phpstan/ <- your scan result directory
βββ app/
βββ config/
βββ database/
βββ routes/
βββ support/
βββ phpstan.neon.dist <- your PHPStan config file
βββ bin/
β βββ phpstan <- your custom bash script
βββ vendor/
π File Setup
Create .phpstan
directory:
mkdir -p .phpstan
Add .gitignore
file in .phpstan
directory and paste the following content. This to make sure you don't commit the files generated in your code repository each time the script execute.
*
!.gitignore
Then create a file at bin/phpstan
in your Laravel project:
mkdir -p bin
touch bin/phpstan
chmod +x bin/phpstan
Paste the full script below into bin/phpstan
:
#!/usr/bin/env bash
clear
echo "Running PHPStan..."
vendor/bin/phpstan --error-format=json > .phpstan/scan-result.json
jq . .phpstan/scan-result.json > .phpstan/scan-result.pretty.json && mv .phpstan/scan-result.pretty.json .phpstan/scan-result.json
input_file=".phpstan/scan-result.json"
output_dir=".phpstan"
if [[ ! -f "$input_file" ]]; then
echo "β File $input_file not found."
exit 1
fi
find "$output_dir" -type f -name '*.txt' -delete
# Validate if the JSON has a "files" key and it's not null
if ! jq -e '.files != null and (.files | length > 0)' "$input_file" >/dev/null; then
echo "βΉοΈ No issues found or invalid PHPStan JSON output."
exit 0
fi
mkdir -p "$output_dir"
echo "π Output directory ready: $output_dir"
echo "π Reading from: $input_file"
jq -r '
.files as $files |
$files | to_entries[] |
.key as $file |
.value.messages[] |
[
.identifier,
$file,
(.line | tostring),
.message,
(if (.tip != null and (.tip | type) == "string") then .tip else "" end),
(if (.ignorable == true) then "Yes" else "No" end)
] | @tsv
' "$input_file" |
while IFS=$'\t' read -r identifier file line message tip ignorable; do
output_file="${output_dir}/${identifier}.txt"
{
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo "π File : $file"
echo "π’ Line : $line"
echo "π¬ Message : $message"
[[ -n "$tip" ]] && echo "π‘ Tip : $tip"
echo "β
Ignorable : $ignorable"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
} >> "$output_file"
done
echo "β
PHPStan scan identifiers outputted into individual files."
# Generate summary
summary_file="${output_dir}/summary.txt"
# Define label width (adjust if needed)
label_width=42
total_issues=0
total_identifiers=0
# Temp file to collect identifier and count lines
temp_summary_data=$(mktemp)
# Loop through all identifier files
for file in "$output_dir"/*.txt; do
[[ "$file" == "$summary_file" ]] && continue
identifier=$(basename "$file" .txt)
count=$(grep -c "π File" "$file")
printf -- "- %-${label_width}s : %4d\n" "$identifier" "$count" >> "$temp_summary_data"
total_issues=$((total_issues + count))
total_identifiers=$((total_identifiers + 1))
done
# Write summary file using grouped commands
{
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo "π PHPStan Scan Summary"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
printf -- "- %-${label_width}s : %4d\n" "Unique Identifiers" "$total_identifiers"
printf -- "- %-${label_width}s : %4d\n" "Issues Found" "$total_issues"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo "π Issues by Identifier:"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
sort "$temp_summary_data"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
} > "$summary_file"
echo "π Summary written to $summary_file"
# Clean up
rm -f "$temp_summary_data"
π Running the Script
To execute the script and generate a readable report:
bin/phpstan
It will run PHPStan, store the raw output in .phpstan/scan-result.json
, and generate the following:
-
.phpstan/summary.txt
β π A clean summary of issues grouped by identifier. -
.phpstan/*.txt
β π Detailed issue logs grouped by identifier.
π Sample Summary Output
Hereβs an example of what youβll get in .phpstan/summary.txt
:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
π PHPStan Scan Summary
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Unique Identifiers : 1
- Issues Found : 468
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Issues by Identifier:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
- property.notFound : 468
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
π§© Sample Issue Breakdown
For every unique issue identifier (e.g., property.notFound
), you get a dedicated .txt
file like:
.phpstan/property.notFound.txt
With formatted entries:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π File : /app/Http/Controllers/Admin/LetterTemplates/GeneratePdfController.php
π’ Line : 90
π¬ Message : Access to an undefined property App\Models\ApplicantProgramme::$name.
π‘ Tip : Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property
β
Ignorable : Yes
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Benefits
- Easier triaging of errors based on identifiers (e.g.,
property.notFound
,callToUndefinedMethod
) - Helps team reviews and prioritising fix strategies
- Outputs are readable, even for non-technical stakeholders
- Can be integrated into CI pipelines or daily QA routines
- Improve team code quality and practices
Great addition! If you're using Laravel Pint alongside PHPStan + Larastan, it's important to avoid conflicts caused by over-aggressive PHPDoc cleanups.
Here's a short guide you can include in your documentation or blog post:
β¨ Using Laravel Pint with PHPStan & Larastan
Laravel Pint is a zero-configuration code style fixer for Laravel, powered by PHP-CS-Fixer.
However, some default Pint rules may strip or alter PHPDoc annotations that are required for Larastanβs type analysis.
To avoid this, you should disable PHPDoc-related cleanup rules in your Pint configuration.
β
Recommended pint.json
Configuration
Create or update your pint.json
file in the project root:
{
"preset": "laravel",
"rules": {
"no_superfluous_phpdoc_tags": false,
"no_empty_phpdoc": false,
"phpdoc_no_empty_return": false,
"phpdoc_no_useless_inheritdoc": false,
"phpdoc_trim": false,
"phpdoc_trim_consecutive_blank_line_separation": false,
"general_phpdoc_annotation_remove": false,
"phpdoc_annotation_without_dot": false,
"phpdoc_summary": false,
"phpdoc_separation": false,
"phpdoc_single_line_var_spacing": false,
"phpdoc_to_comment": false,
"phpdoc_tag_type": false,
"phpdoc_var_without_name": false,
"phpdoc_align": false,
"phpdoc_indent": false,
"phpdoc_inline_tag_normalizer": false,
"phpdoc_no_alias_tag": false,
"phpdoc_no_package": false,
"phpdoc_scalar": false,
"phpdoc_types": false,
"phpdoc_types_order": false,
"phpdoc_var_annotation_correct_order": false
},
"exclude": [
"vendor",
"node_modules",
"storage",
"bootstrap/cache"
]
}
This configuration disables rules that would otherwise remove, reformat, or reorder your @phpdoc
annotations β preserving the type hints needed by Larastan.
Certainly! Here's an additional section you can include in your blog post or documentation, explaining how to ask AI (like ChatGPT or GitHub Copilot) to help fix PHPStan issues, with a real-world example:
π€ Using AI to Fix PHPStan Issues
Once you've generated your .phpstan/summary.txt
and reviewed issues grouped by identifier (like property.notFound
), you can copy a specific block from .phpstan/*.txt
and ask an AI tool (e.g., ChatGPT) for help in fixing it.
π Sample Prompt to AI
Here's a recommended format for your prompt:
I'm using Laravel and ran PHPStan with Larastan. I got this error:
π File : app/Http/Controllers/Admin/LetterTemplates/GeneratePdfController.php
π’ Line : 90
π¬ Message : Access to an undefined property App\Models\ApplicantProgramme::$name.
π‘ Tip : Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property
Can you help me fix this issue? How should I define this property in the model?
β AI-Friendly Tips
- Include the file path and line number.
- Mention you're using Laravel so the AI understands the context.
- Paste the full error block.
- Optionally: Include the relevant model or controller code to give more context.
π¬ Example AI Conversation
You:
I'm using Laravel and got this PHPStan error:
Access to an undefined property App\Models\ApplicantProgramme::$name.
Here's my model (partial):
class ApplicantProgramme extends Model { protected $table = 'applicant_programmes'; }
How can I fix it?
AI:
You can resolve this issue by adding a PHPDoc property annotation in your model:
/** * @property string $name */ class ApplicantProgramme extends Model { protected $table = 'applicant_programmes'; }
This tells PHPStan and IDEs that the
$name
property exists, even if it's not explicitly defined in the class (since it's a dynamic Eloquent attribute).
π Bonus Tip: Use ChatGPT with .phpstan/summary.txt
After running your bin/phpstan
script and generating .phpstan/summary.txt
, you can even copy-paste multiple grouped issues and ask:
βHere are all my
property.notFound
issues from PHPStan scan in a Laravel project. Can you help me review and recommend how to resolve them?β
π§΅ Final Words
Clean code isnβt just about style β itβs about safety and confidence in your application.
This script enhances PHPStanβs power by making its insights more actionable.
Do go with lower level before you start with higher rule level. Level 4 is just nice to start with.
Try it in your project and let me know how it works for you!
Photo by Hitesh Choudhary on Unsplash
Top comments (0)