This content originally appeared on DEV Community and was authored by Nasrul Hazim Bin Mohamad
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
This content originally appeared on DEV Community and was authored by Nasrul Hazim Bin Mohamad