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:
- Formatting: Laravel Pint
- Static Analysis: PHPStan level 6+
- Tests: PHPUnit with 80%+ coverage
- Security:
composer audit - Git Hooks: Pre-commit for format + analyze
- 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.