Code Quality

Linting & Code Quality

Tools Overview

Tool Purpose
Laravel Pint Code formatting (PHP CS Fixer)
PHPStan + Larastan Static analysis
Rector Automated refactoring

Laravel Pint

Configuration

// pint.json
{
    "preset": "laravel",
    "rules": {
        "align_multiline_comment": true,
        "array_indentation": true,
        "array_syntax": true,
        "binary_operator_spaces": {
            "operators": {
                "=>": "align_single_space_minimal"
            }
        },
        "blank_line_before_statement": {
            "statements": ["continue", "return", "throw", "try"]
        },
        "class_attributes_separation": {
            "elements": {
                "method": "one",
                "trait_import": "none"
            }
        },
        "concat_space": {
            "spacing": "one"
        },
        "date_time_immutable": true,
        "declare_strict_types": true,
        "final_class": true,
        "final_public_method_for_abstract_class": true,
        "fully_qualified_strict_types": true,
        "global_namespace_import": {
            "import_classes": true,
            "import_constants": true,
            "import_functions": true
        },
        "multiline_whitespace_before_semicolons": {
            "strategy": "no_multi_line"
        },
        "new_with_parentheses": {
            "anonymous_class": false,
            "named_class": false
        },
        "no_unused_imports": true,
        "nullable_type_declaration_for_default_null_value": true,
        "ordered_class_elements": {
            "order": [
                "use_trait",
                "case",
                "constant_public",
                "constant_protected",
                "constant_private",
                "property_public",
                "property_protected",
                "property_private",
                "construct",
                "destruct",
                "magic",
                "phpunit",
                "method_public",
                "method_protected",
                "method_private"
            ]
        },
        "ordered_imports": {
            "imports_order": ["class", "function", "const"],
            "sort_algorithm": "alpha"
        },
        "ordered_interfaces": true,
        "ordered_traits": true,
        "phpdoc_order": true,
        "phpdoc_separation": true,
        "phpdoc_summary": true,
        "self_static_accessor": true,
        "simplified_null_return": true,
        "single_line_empty_body": true,
        "strict_comparison": true,
        "trailing_comma_in_multiline": true,
        "visibility_required": {
            "elements": ["method", "property"]
        },
        "void_return": true,
        "yoda_style": {
            "equal": false,
            "identical": false,
            "less_and_greater": false
        }
    }
}

Running Pint

# Format all files
./vendor/bin/pint

# Check without fixing (CI)
./vendor/bin/pint --test

# Format specific path
./vendor/bin/pint app/Models

# Verbose output
./vendor/bin/pint -v

Key Rules Explained

Rule Effect
declare_strict_types Adds declare(strict_types=1);
final_class Makes classes final
strict_comparison Uses === instead of ==
void_return Adds void return types
new_with_parentheses: false new User instead of new User()
trailing_comma_in_multiline Trailing commas in arrays/params
ordered_imports Alphabetical imports

PHPStan

Configuration

# phpstan.neon
includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/nesbot/carbon/extension.neon

parameters:
    paths:
        - app
        - database
        - routes
        - tests

    level: 5

    checkModelProperties: true

    ignoreErrors:
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::ordered\(\)#'
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::attachTags\(\)#'
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::syncTags\(\)#'

    excludePaths:
        - vendor
        - storage
        - bootstrap/cache

Levels

Level Checks
0 Basic checks, unknown classes
1 Possibly undefined variables
2 Unknown methods on $this
3 Return types, types of properties
4 Basic dead code checking
5 Argument types (recommended)
6 Report missing typehints
7 Union types
8 Nullable types
9 Mixed type

Running PHPStan

# Run analysis
./vendor/bin/phpstan analyse

# With memory limit
./vendor/bin/phpstan analyse --memory-limit=512M

# Generate baseline (ignore existing errors)
./vendor/bin/phpstan analyse --generate-baseline

# Specific path
./vendor/bin/phpstan analyse app/Models

Common Fixes

// Error: Property has no type
// Fix: Add type declaration
private int $count;

// Error: Method returns null but return type doesn't allow it
// Fix: Make return type nullable
public function find(int $id): ?User

// Error: Parameter $data has no type
// Fix: Add parameter type
public function process(array $data): void

Rector

Configuration

// rector.php
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
use RectorLaravel\Set\LaravelSetList;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/app',
        __DIR__ . '/database',
        __DIR__ . '/routes',
        __DIR__ . '/tests',
    ])
    ->withSkip([
        __DIR__ . '/bootstrap/cache',
        __DIR__ . '/storage',
        __DIR__ . '/vendor',
    ])
    ->withSets([
        SetList::DEAD_CODE,
        SetList::CODE_QUALITY,
        SetList::TYPE_DECLARATION,
        SetList::PRIVATIZATION,
        SetList::EARLY_RETURN,
        SetList::STRICT_BOOLEANS,
        LaravelSetList::LARAVEL_110,
    ])
    ->withPreparedSets(
        deadCode: true,
        codeQuality: true,
        typeDeclarations: true,
        privatization: true,
        earlyReturn: true,
        strictBooleans: true,
    );

Running Rector

# Preview changes (dry run)
./vendor/bin/rector process --dry-run

# Apply changes
./vendor/bin/rector process

# Specific path
./vendor/bin/rector process app/Models

# With specific rule
./vendor/bin/rector process --only="Rector\\DeadCode\\Rector\\ClassMethod\\RemoveUnusedPrivateMethodRector"

Common Transformations

// BEFORE: Dead code removal
private function unusedMethod() { }

// AFTER: Method removed

// BEFORE: Early return
public function check($value)
{
    if ($value) {
        return true;
    } else {
        return false;
    }
}

// AFTER: Simplified
public function check($value): bool
{
    return (bool) $value;
}

// BEFORE: Missing return type
public function getName()
{
    return $this->name;
}

// AFTER: Type added
public function getName(): string
{
    return $this->name;
}

Composer Scripts

Add to composer.json:

{
    "scripts": {
        "lint": "./vendor/bin/pint",
        "lint:check": "./vendor/bin/pint --test",
        "analyse": "./vendor/bin/phpstan analyse",
        "refactor": "./vendor/bin/rector process --dry-run",
        "refactor:apply": "./vendor/bin/rector process",
        "test": "./vendor/bin/pest",
        "quality": [
            "@lint:check",
            "@analyse",
            "@test"
        ]
    }
}

Usage:

composer lint        # Format code
composer analyse     # Static analysis
composer test        # Run tests
composer quality     # All checks

CI/CD Integration

GitHub Actions Example

# .github/workflows/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'
          coverage: none

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

      - name: Run Pint
        run: ./vendor/bin/pint --test

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse

      - name: Run Tests
        run: ./vendor/bin/pest

Editor Integration

VS Code

// .vscode/settings.json
{
    "editor.formatOnSave": true,
    "[php]": {
        "editor.defaultFormatter": "open-southeners.laravel-pint"
    }
}

PHPStorm

  1. Settings → PHP → Quality Tools → Laravel Pint
  2. Enable "Run on save"
  3. Point to ./vendor/bin/pint

Pre-Commit Hooks

Using Husky or similar:

#!/bin/sh
# .husky/pre-commit

./vendor/bin/pint --test
./vendor/bin/phpstan analyse
./vendor/bin/pest --parallel