Code Review Automation and Tooling

Reverend Philip Dec 18, 2025 3 min read

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

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

        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:

# Install
composer require laravel/pint --dev

# Run
./vendor/bin/pint

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

# Specific files
./vendor/bin/pint app/Models
// pint.json
{
    "preset": "laravel",
    "rules": {
        "simplified_null_return": true,
        "blank_line_before_statement": {
            "statements": ["return", "throw", "try"]
        }
    },
    "exclude": [
        "vendor"
    ]
}

PHP CS Fixer

More configuration options:

// .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'])
    );

Static Analysis

PHPStan

Finds bugs without running code:

# Install
composer require --dev phpstan/phpstan

# Run
./vendor/bin/phpstan analyse app tests --level=8
# 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

Psalm

Alternative with different strengths:

# Install
composer require --dev vimeo/psalm

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

# Run
./vendor/bin/psalm
<!-- 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>

Complexity Analysis

PHP Mess Detector

Catches complexity and potential issues:

./vendor/bin/phpmd app text cleancode,codesize,controversial,design,naming,unusedcode
<!-- 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>

Cognitive Complexity

// 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

Security Scanning

Local Security Checker

# Check for known vulnerabilities in dependencies
composer audit

Automated Security Analysis

# 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

# 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

Git Hooks

Pre-commit Hooks

#!/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

Husky + lint-staged

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

CI/CD Integration

GitHub Actions

# .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

GitLab CI

# .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:

// 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.');
}

PR Review Bot

# .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

IDE Integration

VS Code Settings

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

PhpStorm Inspections

<!-- .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

// 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 [];
    }
}

Pint Custom Fixer

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

Metrics Dashboard

PHPMetrics

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

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

SonarQube Integration

# 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

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

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

API Design First Development

Design APIs before implementing them. Use OpenAPI specifications, mock servers, and contract-first workflows.

Jan 15, 2026

Need help with your project?

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