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
- What is Laravel Pint?
- Why Use Laravel Pint?
- Installation
- Configuration Explained
- Rule Categories
- Running Pint
- Best Practices
- 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
DateTimeImmutableoverDateTime
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
- Memory Issues with Large Files
php -d memory_limit=512M ./vendor/bin/pint
- Exclude Problematic Files
{
"notPath": ["app/Legacy/*", "database/migrations/*"]
}
- 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
- Check if Pint is already installed before reaching for Composer:
./vendor/bin/pint --version - Run
./vendor/bin/pintto format your code - Customise rules in
pint.jsonas needed - Add it to your CI/CD pipeline with
--testmode - Set up a pre-commit hook to catch issues before they reach the branch