paulund
#laravel #php #code-style

Laravel Pint: Opinionated PHP Code Style Fixer

Laravel Pint is an opinionated PHP code style fixer built on top of PHP-CS-Fixer. It provides zero-configuration code formatting for Laravel projects while offering extensive customisation options for teams that need specific coding standards.

Table of Contents

  1. What is Laravel Pint?
  2. Why Use Laravel Pint?
  3. Installation
  4. Configuration Explained
  5. Rule Categories
  6. Running Pint
  7. Best Practices
  8. Advanced Configuration

What is Laravel Pint?

Laravel Pint is a zero-configuration PHP code style fixer for minimalists. It's built on top of PHP-CS-Fixer and provides:

  • Opinionated defaults - Works out of the box with Laravel conventions
  • Customisable rules - Extensive configuration options for team preferences
  • Fast execution - Optimised for performance with parallel processing
  • Laravel integration - Seamless integration with Laravel projects

Pint ensures your code follows consistent formatting standards without requiring complex configuration.

Why Use Laravel Pint?

1. Zero Configuration

Works immediately with sensible Laravel defaults:

  • No setup required for basic Laravel projects
  • Follows Laravel's official coding standards
  • Handles PSR-12 compliance automatically

2. Consistent Code Style

Eliminates style debates and inconsistencies:

  • Uniform formatting across the entire codebase
  • Automatic fixing of common style issues
  • Consistent indentation, spacing, and structure

3. Developer Productivity

Reduces mental overhead and code review time:

  • Developers focus on logic, not formatting
  • Automated formatting in CI/CD pipelines
  • Fewer style-related code review comments

4. Team Collaboration

Ensures consistent code across team members:

  • Eliminates "style wars" between developers
  • Consistent code regardless of IDE or editor
  • Onboarding new team members with established standards

Installation

Laravel Projects

Laravel includes Pint as a dev dependency, so it's likely already in your project:

# Check if Pint is installed
./vendor/bin/pint --version

# If not installed, add it manually
composer require laravel/pint --dev

For Non-Laravel Projects

composer require laravel/pint --dev

Configuration Explained

Here's the configuration file (pint.json) with detailed explanations:

{
    "preset": "laravel",
    "notPath": ["tests/TestCase.php"],
    "rules": {
        "array_push": true,
        "backtick_to_shell_exec": true,
        "date_time_immutable": true,
        "declare_strict_types": true,
        "lowercase_keywords": true,
        "lowercase_static_reference": true,
        "final_class": true,
        "final_internal_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
        },
        "mb_str_functions": true,
        "modernize_types_casting": true,
        "new_with_parentheses": false,
        "no_superfluous_elseif": true,
        "no_useless_else": true,
        "no_multiple_statements_per_line": true,
        "ordered_class_elements": {
            "order": [
                "use_trait",
                "case",
                "constant",
                "constant_public",
                "constant_protected",
                "constant_private",
                "property_public",
                "property_protected",
                "property_private",
                "construct",
                "destruct",
                "magic",
                "phpunit",
                "method_abstract",
                "method_public_static",
                "method_public",
                "method_protected_static",
                "method_protected",
                "method_private_static",
                "method_private"
            ],
            "sort_algorithm": "none"
        },
        "ordered_interfaces": true,
        "ordered_traits": true,
        "protected_to_private": true,
        "self_accessor": true,
        "self_static_accessor": true,
        "strict_comparison": true,
        "visibility_required": true
    }
}

Configuration Breakdown

1. Preset Configuration

"preset": "laravel"
  • Purpose: Uses Laravel's official coding standards as the base
  • Alternatives: "psr12", "symfony", "per"
  • Effect: Applies Laravel-specific formatting rules

2. Path Exclusions

"notPath": ["tests/TestCase.php"]
  • Purpose: Excludes specific files from formatting
  • Common exclusions: Generated files, third-party code, legacy files
  • Example: ["bootstrap/cache/*", "storage/*", "vendor/*"]

3. Rule Categories

Modern PHP Features:

"declare_strict_types": true,
"modernize_types_casting": true,
"date_time_immutable": true
  • Enforces strict typing declarations
  • Modernises type casting syntax
  • Prefers DateTimeImmutable over DateTime

Code Quality Rules:

"final_class": true,
"final_internal_class": true,
"protected_to_private": true
  • Makes classes final when possible
  • Reduces visibility scope when appropriate
  • Improves encapsulation

Import Organisation:

"global_namespace_import": {
    "import_classes": true,
    "import_constants": true,
    "import_functions": true
}
  • Organises use statements
  • Imports global functions and constants
  • Reduces fully qualified name usage

Class Structure:

"ordered_class_elements": {
    "order": [
        "use_trait",
        "case",
        "constant",
        "property_public",
        "property_protected",
        "property_private",
        "construct",
        "destruct",
        "magic",
        "phpunit",
        "method_public",
        "method_protected",
        "method_private"
    ]
}
  • Enforces consistent class member ordering
  • Improves code readability
  • Standardises class structure

Rule Categories

1. Safety and Type Safety

{
    "declare_strict_types": true,
    "strict_comparison": true,
    "fully_qualified_strict_types": true
}

Before:

<?php

class Calculator
{
    public function add($a, $b)
    {
        if ($a == null) {
            return $b;
        }
        return $a + $b;
    }
}

After:

<?php

declare(strict_types=1);

class Calculator
{
    public function add(int $a, int $b): int
    {
        if ($a === null) {
            return $b;
        }
        return $a + $b;
    }
}

2. Code Organisation

{
    "ordered_class_elements": {
        "order": [
            "use_trait",
            "constant",
            "property_public",
            "property_protected",
            "property_private",
            "construct",
            "method_public",
            "method_protected",
            "method_private"
        ]
    }
}

Before:

class User
{
    public function getName(): string
    {
        return $this->name;
    }

    private string $name;

    public const STATUS_ACTIVE = 'active';

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

After:

class User
{
    public const STATUS_ACTIVE = 'active';

    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

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

3. Modernisation Rules

{
    "modernize_types_casting": true,
    "mb_str_functions": true,
    "array_push": true
}

Before:

$string = (string) $value;
$length = strlen($text);
array_push($items, $newItem);

After:

$string = (string) $value;
$length = mb_strlen($text);
$items[] = $newItem;

4. Control Flow Improvements

{
    "no_superfluous_elseif": true,
    "no_useless_else": true,
    "no_multiple_statements_per_line": true
}

Before:

if ($condition1) {
    return 'A';
} elseif ($condition2) {
    return 'B';
} else {
    return 'C';
}

$x = 1; $y = 2;

After:

if ($condition1) {
    return 'A';
}

if ($condition2) {
    return 'B';
}

return 'C';

$x = 1;
$y = 2;

Running Pint

1. Basic Usage

# Format all files
./vendor/bin/pint

# Format specific directory
./vendor/bin/pint app/

# Format specific file
./vendor/bin/pint app/Models/User.php

2. Preview Mode

# See what would be changed without applying
./vendor/bin/pint --test

# Verbose output showing all changes
./vendor/bin/pint --test -v

3. Configuration Options

# Use custom config file
./vendor/bin/pint --config=custom-pint.json

# Use specific preset
./vendor/bin/pint --preset=psr12

# Format only dirty files (git)
./vendor/bin/pint --dirty

4. CI/CD Integration

# Test mode for CI (exits with error if formatting needed)
./vendor/bin/pint --test

# Combine with other tools
./vendor/bin/pint && ./vendor/bin/phpstan analyse

Best Practices

1. Pre-commit Hooks

Install a pre-commit hook to ensure code is formatted before commits:

# Install pre-commit hook
echo '#!/bin/sh
./vendor/bin/pint --test
' > .git/hooks/pre-commit

chmod +x .git/hooks/pre-commit

2. IDE Integration

Configure your IDE to run Pint on save:

VS Code (.vscode/settings.json):

{
    "emeraldwalk.runonsave": {
        "commands": [
            {
                "match": "\\.php$",
                "cmd": "./vendor/bin/pint ${file}"
            }
        ]
    }
}

3. CI/CD Pipeline

# .github/workflows/pint.yml
name: Laravel Pint
on: [push, pull_request]

jobs:
    pint:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: shivammathur/setup-php@v2
              with:
                  php-version: "8.3"
            - run: composer install --optimize-autoloader
            - run: ./vendor/bin/pint --test

4. Team Workflow

# Developer workflow
git add .
./vendor/bin/pint
git add .
git commit -m "Your commit message"

# Or create alias
alias pint-commit="./vendor/bin/pint && git add . && git commit"

Advanced Configuration

Custom Preset

Create a custom preset for your organisation:

{
    "preset": "psr12",
    "rules": {
        "array_syntax": { "syntax": "short" },
        "binary_operator_spaces": {
            "default": "single_space"
        },
        "blank_line_after_namespace": true,
        "blank_line_after_opening_tag": true,
        "blank_line_before_statement": {
            "statements": ["return", "throw", "try"]
        },
        "class_definition": {
            "single_line": true,
            "single_item_single_line": true
        },
        "concat_space": { "spacing": "one" },
        "method_argument_space": {
            "on_multiline": "ensure_fully_multiline"
        },
        "no_unused_imports": true,
        "ordered_imports": {
            "sort_algorithm": "alpha"
        },
        "trailing_comma_in_multiline": {
            "elements": ["arrays", "arguments", "parameters"]
        }
    }
}

Environment-specific Configuration

{
    "preset": "laravel",
    "exclude": ["bootstrap/cache", "storage"],
    "notPath": ["tests/TestCase.php", "database/migrations"],
    "rules": {
        "declare_strict_types": true,
        "final_class": false,
        "php_unit_test_class_requires_covers": false
    }
}

Integration with Other Tools

Combined with Rector:

#!/bin/bash
# Format and refactor script
./vendor/bin/rector process
./vendor/bin/pint
./vendor/bin/phpstan analyse

Package.json Scripts:

{
    "scripts": {
        "format": "./vendor/bin/pint",
        "format:test": "./vendor/bin/pint --test",
        "format:dirty": "./vendor/bin/pint --dirty"
    }
}

Common Configuration Examples

Laravel API Project

{
    "preset": "laravel",
    "rules": {
        "declare_strict_types": true,
        "final_class": true,
        "ordered_class_elements": {
            "order": [
                "use_trait",
                "constant",
                "property_public",
                "property_protected",
                "property_private",
                "construct",
                "method_public",
                "method_protected",
                "method_private"
            ]
        },
        "php_unit_test_class_requires_covers": false,
        "visibility_required": true
    }
}

Legacy Project Migration

{
    "preset": "psr12",
    "rules": {
        "declare_strict_types": false,
        "final_class": false,
        "modernize_types_casting": true,
        "no_superfluous_elseif": true,
        "no_useless_else": true,
        "protected_to_private": false,
        "strict_comparison": false
    }
}

Troubleshooting

Common Issues

  1. Memory Issues with Large Files
php -d memory_limit=512M ./vendor/bin/pint
  1. Exclude Problematic Files
{
    "notPath": ["app/Legacy/*", "database/migrations/*"]
}
  1. Rule Conflicts
{
    "rules": {
        "final_class": false,
        "final_internal_class": false
    }
}

Conclusion

Laravel Pint does a good job of balancing zero-configuration simplicity with the customisation options teams actually need. For new projects, you can drop it in and run it immediately. For teams with specific conventions, pint.json gives you enough control to enforce them without writing a wall of configuration.

The key is to get it into your workflow early. Add it to your CI pipeline, set up a pre-commit hook, and let it handle the formatting decisions so your team can focus on the code that actually matters.

Getting Started

  1. Check if Pint is already installed before reaching for Composer: ./vendor/bin/pint --version
  2. Run ./vendor/bin/pint to format your code
  3. Customise rules in pint.json as needed
  4. Add it to your CI/CD pipeline with --test mode
  5. Set up a pre-commit hook to catch issues before they reach the branch

Related Resources