Code Review Automation and Tooling

Philip Rehberger Dec 18, 2025 6 min read

Automate code review with static analysis, linters, and CI checks. Free human reviewers to focus on architecture and logic.

Code Review Automation and Tooling

Code review is critical for maintaining quality, but manual review doesn't scale. Automation handles routine checks, freeing reviewers to focus on architecture, logic, and design. This guide covers tools and strategies for automating code review in PHP projects.

The Automation Pyramid

Understanding what to automate helps you allocate review effort effectively. Lower levels should be fully automated, allowing human reviewers to focus on higher-level concerns.

        Manual Review
       (Architecture, design)
          /         \
      Automated Analysis
    (Complexity, patterns)
        /             \
   Static Analysis      Tests
  (Types, bugs)      (Behavior)
      /                   \
    Linting            Formatting
  (Standards)           (Style)

Lower levels should be fully automated. Manual review focuses on what machines can't judge.

Code Formatting

Laravel Pint

Laravel's official code style fixer:

Pint enforces consistent code formatting across your project. By automating formatting, you eliminate style debates in code review and ensure every file follows the same conventions. Here's how to get started with Pint in your project.

# Install
composer require laravel/pint --dev

# Run
./vendor/bin/pint

# Check without fixing
./vendor/bin/pint --test

# Specific files
./vendor/bin/pint app/Models

Customize Pint's behavior with a configuration file. The Laravel preset works well for most projects, but you can override individual rules as needed.

// pint.json
{
    "preset": "laravel",
    "rules": {
        "simplified_null_return": true,
        "blank_line_before_statement": {
            "statements": ["return", "throw", "try"]
        }
    },
    "exclude": [
        "vendor"
    ]
}

The exclude array prevents Pint from modifying third-party code, which you should never format as part of your project.

PHP CS Fixer

More configuration options:

For projects requiring more granular control over formatting rules, PHP CS Fixer offers extensive configuration options beyond what Pint provides. You define rules in a PHP configuration file.

// .php-cs-fixer.php
<?php
return (new PhpCsFixer\Config())
    ->setRules([
        '@PSR12' => true,
        '@PHP82Migration' => true,
        'array_syntax' => ['syntax' => 'short'],
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'no_unused_imports' => true,
        'single_quote' => true,
        'trailing_comma_in_multiline' => true,
    ])
    ->setFinder(
        PhpCsFixer\Finder::create()
            ->in(__DIR__)
            ->exclude(['vendor', 'storage', 'bootstrap/cache'])
    );

The @PHP82Migration rule set automatically applies fixes for PHP 8.2 compatibility, helping you adopt new language features consistently.

Static Analysis

PHPStan

Finds bugs without running code:

PHPStan analyzes your code structure to find bugs that would otherwise only appear at runtime. It catches issues like undefined methods, incorrect argument types, and unreachable code. Getting started is straightforward.

# Install
composer require --dev phpstan/phpstan

# Run
./vendor/bin/phpstan analyse app tests --level=8

Configure PHPStan with a neon file to include Laravel-specific rules and customize error handling for your project's needs.

# phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon

parameters:
    level: 8
    paths:
        - app
        - tests
    excludePaths:
        - app/Http/Middleware/TrustProxies.php
    ignoreErrors:
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'
    checkMissingIterableValueType: false

The Larastan extension understands Laravel's magic methods and facades, dramatically reducing false positives. Start at a lower level and gradually increase as you fix existing issues.

Psalm

Alternative with different strengths:

Psalm offers similar functionality to PHPStan with some unique features like more sophisticated generics support and security analysis. Many teams run both tools for comprehensive coverage.

# Install
composer require --dev vimeo/psalm

# Initialize
./vendor/bin/psalm --init

# Run
./vendor/bin/psalm

The XML configuration file controls which directories to analyze and what error level to enforce.

<!-- psalm.xml -->
<?xml version="1.0"?>
<psalm errorLevel="4">
    <projectFiles>
        <directory name="app" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
    <plugins>
        <pluginClass class="Psalm\LaravelPlugin\Plugin"/>
    </plugins>
</psalm>

The Laravel plugin is essential for accurate analysis. Without it, Psalm won't understand facades, model relationships, or other Laravel conventions.

Complexity Analysis

PHP Mess Detector

Catches complexity and potential issues:

PHPMD identifies code that may be difficult to maintain, such as overly complex methods, unused parameters, and potential bugs. It complements static analysis by focusing on code quality rather than correctness.

./vendor/bin/phpmd app text cleancode,codesize,controversial,design,naming,unusedcode

Customize the rules to match your team's standards. Some defaults may be too strict or too lenient for your codebase.

<!-- phpmd.xml -->
<?xml version="1.0"?>
<ruleset name="Custom Rules">
    <rule ref="rulesets/cleancode.xml">
        <exclude name="StaticAccess"/>
    </rule>
    <rule ref="rulesets/codesize.xml">
        <properties>
            <property name="minimum" value="200"/>
        </properties>
    </rule>
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity">
        <properties>
            <property name="reportLevel" value="15"/>
        </properties>
    </rule>
</ruleset>

Excluding StaticAccess is common in Laravel projects since facades are a core part of the framework.

Cognitive Complexity

Cognitive complexity measures how difficult code is to understand, not just how many paths exist. Compare these two approaches to the same problem - the first has deeply nested conditionals while the second uses early returns.

// Bad: High cognitive complexity
function processOrder($order) {
    if ($order->isPaid()) {                    // +1
        if ($order->hasItems()) {              // +2 (nested)
            foreach ($order->items as $item) { // +3 (nested)
                if ($item->inStock()) {        // +4 (nested)
                    // ...
                } else {                       // +1
                    if ($item->canBackorder()) { // +5 (nested)
                        // ...
                    }
                }
            }
        }
    }
}
// Total: 16+ - hard to understand

// Good: Flat structure with early returns
function processOrder($order) {
    if (!$order->isPaid()) {
        return;                                // +1
    }

    if (!$order->hasItems()) {
        return;                                // +1
    }

    foreach ($order->items as $item) {         // +1
        $this->processItem($item);
    }
}
// Total: 3 - easy to follow

Early returns flatten the code structure and reduce nesting. Each level of nesting multiplies cognitive load.

Security Scanning

Local Security Checker

Composer includes a built-in audit command that checks your dependencies against known vulnerability databases. Run this regularly, ideally as part of your CI pipeline.

# Check for known vulnerabilities in dependencies
composer audit

Automated Security Analysis

Integrate security scanning into your CI pipeline to catch vulnerabilities before they reach production. This workflow step fails the build if any advisories are found.

# Use in CI
- name: Security Check
  run: |
    composer audit --format=json > audit.json
    if [ $(jq '.advisories | length' audit.json) -gt 0 ]; then
      echo "Security vulnerabilities found!"
      exit 1
    fi

SAST Tools

Static Application Security Testing tools scan your own code for security issues, not just dependencies. Semgrep is particularly powerful because you can write custom rules for your project.

# Semgrep for custom rules
semgrep --config=p/php app/

# Example rule for SQL injection
rules:
  - id: raw-sql-injection
    patterns:
      - pattern: DB::raw($USER_INPUT)
    message: "Potential SQL injection"
    severity: ERROR

Custom Semgrep rules let you encode your team's security knowledge and catch project-specific anti-patterns automatically.

Git Hooks

Pre-commit Hooks

Pre-commit hooks run checks before code is committed, catching issues before they enter your repository history. This script formats staged files and runs static analysis.

#!/bin/bash
# .git/hooks/pre-commit

# Run Pint on staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')

if [ -n "$STAGED_FILES" ]; then
    ./vendor/bin/pint $STAGED_FILES
    git add $STAGED_FILES
fi

# Run PHPStan
./vendor/bin/phpstan analyse --no-progress

if [ $? -ne 0 ]; then
    echo "PHPStan found issues. Please fix before committing."
    exit 1
fi

This script formats staged PHP files and runs static analysis. If PHPStan finds issues, the commit is blocked until they're resolved.

Husky + lint-staged

For projects using npm, Husky provides a more maintainable way to manage git hooks with version-controlled configuration. The hooks are defined in package.json rather than shell scripts.

// package.json
{
    "husky": {
        "hooks": {
            "pre-commit": "lint-staged"
        }
    },
    "lint-staged": {
        "*.php": [
            "./vendor/bin/pint",
            "./vendor/bin/phpstan analyse --no-progress"
        ]
    }
}

This configuration only runs tools on staged files, making pre-commit hooks fast even in large codebases.

CI/CD Integration

GitHub Actions

GitHub Actions provides a natural home for automated code quality checks. This workflow runs on every push and pull request to catch issues early.

# .github/workflows/code-quality.yml
name: Code Quality

on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Check code style
        run: ./vendor/bin/pint --test

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --error-format=github

      - name: Run tests
        run: php artisan test --coverage --min=80

The --error-format=github flag makes PHPStan output annotations that appear inline on pull request diffs, making issues immediately visible.

GitLab CI

GitLab CI offers similar capabilities with its own configuration format. The artifacts feature integrates with GitLab's code quality visualization dashboard.

# .gitlab-ci.yml
code_quality:
  stage: test
  script:
    - composer install --prefer-dist --no-progress
    - ./vendor/bin/pint --test
    - ./vendor/bin/phpstan analyse --error-format=gitlab > phpstan.json
  artifacts:
    reports:
      codequality: phpstan.json

Pull Request Automation

Danger PHP

Automate PR feedback:

Danger runs during CI and can comment on pull requests with automated feedback. It's particularly useful for enforcing team conventions that are hard to check with static analysis.

// Dangerfile.php
<?php
use Danger\Danger;

$danger = new Danger();

// Check for tests with new code
$modifiedFiles = $danger->github->pullRequest->changedFiles;
$hasSourceChanges = array_filter($modifiedFiles, fn($f) => str_starts_with($f, 'app/'));
$hasTestChanges = array_filter($modifiedFiles, fn($f) => str_starts_with($f, 'tests/'));

if ($hasSourceChanges && !$hasTestChanges) {
    $danger->warn('This PR modifies source files but has no test changes.');
}

// Check PR size
$additions = $danger->github->pullRequest->additions;
if ($additions > 500) {
    $danger->warn('This PR is quite large. Consider breaking it up.');
}

// Require description
if (strlen($danger->github->pullRequest->body) < 50) {
    $danger->fail('Please provide a detailed description.');
}

These rules codify review expectations. Instead of reviewers repeatedly asking for tests or smaller PRs, the automation handles it.

PR Review Bot

Custom automation can catch issues specific to your project that general tools miss. This workflow checks for common problems like debug statements left in code.

# .github/workflows/pr-review.yml
name: PR Review

on: pull_request

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check file size
        run: |
          LARGE_FILES=$(git diff --stat origin/main...HEAD | grep -E '\|\s+[0-9]{3,}\s+' | wc -l)
          if [ $LARGE_FILES -gt 0 ]; then
            echo "::warning::PR contains large file changes"
          fi

      - name: Check for debugging code
        run: |
          if grep -r "dd(" app/ --include="*.php"; then
            echo "::error::Found dd() statements in code"
            exit 1
          fi
          if grep -r "var_dump\|print_r" app/ --include="*.php"; then
            echo "::error::Found debug statements in code"
            exit 1
          fi

Catching dd() statements before merge prevents accidental debugging code from reaching production.

IDE Integration

VS Code Settings

Configure your IDE to run formatters and analyzers automatically, catching issues as you type rather than at commit time.

// .vscode/settings.json
{
    "editor.formatOnSave": true,
    "[php]": {
        "editor.defaultFormatter": "open-fabrikat.laravel-pint"
    },
    "phpstan.enabled": true,
    "phpstan.level": "8",
    "intelephense.diagnostics.undefinedTypes": false
}

Disabling Intelephense's undefined types check prevents conflicts with PHPStan, which has better Laravel support.

PhpStorm Inspections

PhpStorm's native inspections can be augmented with external tool integration for a comprehensive development experience. Configure inspection profiles to match your project's standards.

<!-- .idea/inspectionProfiles/Project_Default.xml -->
<component name="InspectionProjectProfileManager">
    <profile version="1.0">
        <option name="myName" value="Project Default" />
        <inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="ERROR" />
        <inspection_tool class="PhpStanInspection" enabled="true" level="WARNING" />
    </profile>
</component>

Custom Rules

PHPStan Custom Rule

When your project has specific patterns to enforce, custom PHPStan rules provide precise detection. This example prevents env() calls outside of config files, enforcing a Laravel best practice.

// src/PHPStan/NoDirectEnvAccessRule.php
<?php
namespace App\PHPStan;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;

class NoDirectEnvAccessRule implements Rule
{
    public function getNodeType(): string
    {
        return FuncCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!$node->name instanceof Node\Name) {
            return [];
        }

        if ($node->name->toString() !== 'env') {
            return [];
        }

        $file = $scope->getFile();
        if (!str_contains($file, '/config/')) {
            return ['env() should only be called in config files. Use config() instead.'];
        }

        return [];
    }
}

This rule enforces a Laravel best practice automatically. Once it's in your CI pipeline, you'll never need to manually review for this issue again.

Pint Custom Fixer

For custom formatting rules, Pint supports extension through its configuration. You can reference custom rule classes to enforce project-specific style conventions.

// pint.json
{
    "preset": "laravel",
    "rules": {
        "App\\Pint\\NoInlineVarRule": true
    }
}

Metrics Dashboard

PHPMetrics

PHPMetrics generates visual reports showing code quality trends over time. Run it regularly to track improvement and identify areas needing refactoring.

# Generate HTML report
./vendor/bin/phpmetrics --report-html=metrics/ app/

# Key metrics to track:
# - Cyclomatic complexity
# - Maintainability index
# - Coupling between objects
# - Lines of code

The HTML report provides interactive visualizations that make it easy to identify problem areas in your codebase.

SonarQube Integration

For enterprise environments, SonarQube provides a comprehensive quality platform with historical tracking and team dashboards. Configure your project to send analysis results to SonarQube.

# sonar-project.properties
sonar.projectKey=my-laravel-app
sonar.sources=app
sonar.tests=tests
sonar.php.coverage.reportPaths=coverage/clover.xml
sonar.php.tests.reportPath=tests/junit.xml

SonarQube aggregates results from multiple tools into a single dashboard, making it easier to track quality across large teams.

Recommended Stack

For Laravel projects, start with:

  1. Formatting: Laravel Pint
  2. Static Analysis: PHPStan level 6+
  3. Tests: PHPUnit with 80%+ coverage
  4. Security: composer audit
  5. Git Hooks: Pre-commit for format + analyze
  6. CI: GitHub Actions running all checks

Conclusion

Automate everything that can be automated objectively. Code formatting, type checking, complexity metrics, and security scanning should never require human review. This frees reviewers to focus on what matters: architecture decisions, business logic correctness, and maintainability. Start with formatting and static analysis, add security scanning, then gradually increase strictness as your codebase improves.

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.