Code Quality Check with PHPStan



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