Code quality isnât about preference â itâs about preventing bugs before they happen. In 2023, Laravel developers have access to powerful tools that catch type errors, enforce coding standards, and ensure consistency across teams.
The tools have matured significantly. Static analysis can detect entire classes of bugs that would previously require runtime testing. Automated code styling eliminates formatting debates in code reviews. Together, they create guardrails that let you focus on solving business problems rather than fixing preventable mistakes.
What Youâll Learn
- Configuring PhpStan at maximum strictness (level 9) for Laravel
- Setting up Laravel Pint with custom PSR-12 rules
- Integrating type coverage checks with Pest PHP
- Building CI/CD pipelines for automated quality checks
- Creating a personal workflow that enforces quality before commits
Static Analysis with PhpStan
Static analysis examines your code without running it. It understands type relationships, detects impossible conditions, and catches bugs that would only surface at runtime.
Why PhpStan?
PhpStan analyzes your codeâs type flow and reports inconsistencies. It knows that if a method returns string|null, you canât pass its return value to a function expecting string. It catches calls to undefined methods, mismatched type hints, and unreachable code.
Configuration
Start with this baseline configuration for Laravel applications:
parameters:
paths:
- app/
level: 9
checkGenericClassInNonGenericObjectType: falseLevel 9 is PhpStanâs strictest setting. It enforces complete type safety, requiring you to handle every possible type scenario. The checkGenericClassInNonGenericObjectType: false prevents false positives on Eloquent model relationships, which use magic methods PhpStan canât fully analyze.
Add a composer script for convenience:
{
"scripts": {
"stan": [
"./vendor/bin/phpstan analyse --memory-limit=3g"
]
}
}The explicit memory limit prevents out-of-memory failures on larger projects.
GitHub Actions Integration
Set up a dedicated quality check workflow:
jobs:
quality:
name: "Code Quality Checks"
steps:
- name: 'Run PhpStan'
run: composer run stanKeep this separate from your main test suite. Run it on pull requests and main branch pushes, not on every commit to draft PRs.
Code Styling with Laravel Pint
PHP CS Fixer has been the standard for PHP code styling for years. Laravel Pint is a Laravel-first wrapper that simplifies configuration and provides friendlier output.
Configuration
Hereâs an opinionated but practical Pint configuration that enforces strict typing and clean code:
{
"preset": "per",
"rules": {
"declare_strict_types": true,
"final_class": true,
"fully_qualified_strict_types": true,
"method_chaining_indentation": true,
"ordered_traits": true,
"protected_to_private": true,
"yoda_style": true,
"concat_space": {
"spacing": "one"
},
"ordered_imports": {
"sort_algorithm": "alpha"
},
"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"
]
},
"no_superfluous_elseif": true,
"no_useless_else": true,
"strict_comparison": true,
"ternary_to_null_coalescing": true,
"use_arrow_functions": true,
"void_return": true
}
}Key Configuration Choices
declare_strict_types: true â This is the single most impactful quality setting. It makes PHP throw TypeError when you pass an argument of the wrong type instead of silently coercing it. Every file in your application should begin with declare(strict_types=1).
final_class: true â Making classes final by default forces you to explicitly think about inheritance. Instead of extending a class, you compose with it. This leads to more maintainable code because composition creates explicit contracts while inheritance creates implicit dependencies.
protected_to_private: true â Properties and methods that donât need to be accessed by subclasses should be private. This reduces the surface area of your class and makes refactoring safer.
yoda_style: true â Writing null !== $variable instead of $variable !== null prevents the common mistake of typing $variable = null instead of $variable === null. The parser catches the assignment-in-condition error.
Composer Script and CI
{
"scripts": {
"pint": [
"./vendor/bin/pint"
]
}
}jobs:
quality:
name: "Code Quality Checks"
steps:
- name: 'Run Laravel Pint'
run: composer run pintType Coverage with Pest PHP
Type coverage is a relatively new quality metric. It measures what percentage of your code has explicit type annotations for parameters, return types, and properties.
Pest PHP includes a type coverage plugin that integrates seamlessly:
{
"scripts": {
"types": [
"./vendor/bin/pest --type-coverage"
]
}
}This scans your entire codebase and reports the percentage of typed declarations. Unlike PhpStan, which verifies type correctness, type coverage measures type completeness. Together, they ensure that not only are your types correct, but you havenât missed any declarations.
The Complete Workflow
A disciplined quality workflow follows these steps:
- Write code and tests
- Run static analysis (
composer run stan) - Apply code styling (
composer run pint) - Check type coverage (
composer run types) - Run tests (
composer run test) - Commit
This order matters. Run static analysis before styling because PhpStan provides more actionable feedback. Style after fixing type issues to avoid formatting code that youâll immediately change.
PhpStan Beyond Level 9
While level 9 is the maximum strictness, PhpStan offers features beyond the baseline level check.
Custom Rules
Write custom rules for project-specific conventions:
<?php
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
class NoDumpRule implements Rule
{
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}
public function processNode(Node $node, Scope $scope): array
{
if ($node->name instanceof Node\Name
&& in_array($node->name->toString(), ['dump', 'dd', 'var_dump'], true)
) {
return ['Remove debug call before committing'];
}
return [];
}
}Baseline Management
For existing codebases, generate a baseline to track known issues:
vendor/bin/phpstan analyse --generate-baselineThe baseline file lists current errors. As you fix them, remove them from the baseline. This lets you adopt strict analysis immediately without fixing every issue at once.
Laravel-Specific Extensions
The Larastan extension adds Laravel-specific knowledge to PhpStan:
composer require --dev larastan/larastanIt understands Eloquent model relationships, facade method calls, and macro definitions that vanilla PhpStan would flag as errors.
Laravel Pint in CI/CD
Integrate Pint into your CI pipeline for automated style enforcement:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Check Code Style
run: vendor/bin/pint --testThe --test flag makes Pint check style without modifying files. It returns a non-zero exit code if any files donât match the configured style.
Type Coverage for Complete Safety
Type coverage measures the percentage of your codebase with explicit type annotations. Pest PHPâs type coverage plugin makes this straightforward:
composer require --dev pestphp/pest-plugin-type-coverageConfigure a minimum coverage threshold:
{
"scripts": {
"types": [
"./vendor/bin/pest --type-coverage --min=100"
]
}
}Enforcing 100% type coverage on new code prevents entire categories of bugs. It forces you to declare return types, parameter types, and property types â making your code self-documenting and tool-friendly.
Real-World Use Cases
- Open source packages: Maintainers enforce quality standards across contributions from dozens of developers
- Enterprise teams: Onboarding new developers is faster with automated guardrails that prevent common mistakes
- Agency projects: Consistent code style across multiple client projects makes maintenance predictable
- SaaS products: Type safety prevents production incidents caused by unexpected null values or type mismatches
- Legacy migrations: Static analysis identifies type issues before they cause runtime failures during refactoring
Best Practices
- Start at a lower PhpStan level and work up: Level 9 can be overwhelming for existing codebases. Start at level 5 or 6 and increase gradually
- Run styling before commit: Configure a pre-commit hook to run Pint automatically
- Document baseline exceptions: When you must suppress a PhpStan error, document why in the code
- Treat CI failures as blocking: A PR that fails quality checks should not be mergeable
- Review quality rules quarterly: As your teamâs standards evolve, update your configurations
Common Mistakes to Avoid
- Running static analysis without a baseline: New projects can go straight to level 9. Existing projects need a baseline to track improvements
- Ignoring type coverage: Type declarations prevent an entire category of bugs. Donât skip them because they require more typing
- Applying styling without review: Automated styling can make unexpected changes. Review the diff before committing
- Over-relying on docblocks: PhpStan can parse
@paramand@returnannotations, but native type hints are always preferred - Not updating the baseline: When you fix a PhpStan error, remove it from the baseline. Stale baselines defeat the purpose
Building a Quality Culture
Tools alone donât ensure code quality. The team culture around quality matters more.
Establishing Standards
Write down your teamâs quality standards in an ADR (Architecture Decision Record):
- What PhpStan level is required?
- What Pint rules are enforced?
- What type coverage minimum is acceptable?
- When can exceptions be made and who approves them?
Gradual Enforcement
Introduce strict checks gradually. Start with PhpStan level 5 and increase quarterly. Apply Pint formatting to new code only at first, then expand to the full codebase.
Blame-Free Enforcement
Quality checks catch system issues, not individual failures. A CI pipeline that rejects code for a missed type hint isnât accusing anyone â itâs protecting the team. Frame quality enforcement as collaborative improvement.
Continuous Improvement
Review your quality configuration every quarter. As PHP evolves (new typed properties, readonly classes, enum support), your quality tools can enforce newer language features that prevent even more bugs.
The Cost of Poor Quality
Low code quality has measurable costs:
- Debugging time: Type errors that PhpStan could catch take 10-60 minutes to debug at runtime
- Code review friction: Style inconsistencies generate review comments that distract from logic issues
- Onboarding time: New developers take longer to understand loosely typed, inconsistently formatted code
- Production incidents: Type mismatches cause runtime errors that affect users
- Refactoring risk: Without static analysis, refactoring is terrifying; with it, refactoring is safe
Investing in quality tooling pays for itself in the first few incidents it prevents.
Should I use PhpStan or Psalm? Both are excellent. PhpStan has better Laravel support through extensions. Psalm has more advanced taint analysis. Try both and choose based on your needs.
Can I run Pint on legacy codebases? Yes, with the --test flag to preview changes before applying them. Gradually introduce styling rules to avoid overwhelming diffs.
Does strict typing affect performance? Minimally. The type check happens once at call time. Modern PHP with JIT compilation makes the overhead negligible compared to the bug prevention value.
How do I handle Laravelâs magic methods with PhpStan? Use the @method annotation on model classes or install the larastan extension for comprehensive Laravel support.
Should I enforce final classes on all code? Be pragmatic. Use final by default but make exceptions for classes specifically designed for extension.
Whatâs the right type coverage target? Aim for 100% on new code. For existing codebases, 90%+ is achievable and provides significant quality improvements.
Can I run quality checks in development without CI? Yes. Use tools like husky or pre-commit to run checks before every commit. This catches issues before they reach the repository.
Conclusion
Code quality tools arenât gatekeepers â theyâre accelerators. PhpStan, Laravel Pint, and type coverage catch mistakes that would otherwise require runtime debugging. They enforce consistency that makes code review faster. They create a shared standard that makes onboarding smoother.
Invest time in configuring these tools correctly. The upfront effort of setting up strict rules pays back every single day in reduced debugging time, fewer production incidents, and more predictable development cycles.
Your future self â and your teammates â will thank you for every type hint and every consistent formatting choice.