vimeo/psalm
Psalm is a PHP static analysis tool that finds type issues, bugs, and dead code before runtime. It supports gradual typing via annotations, powerful checks, and configurable rules to improve code quality in applications and libraries.
Installation Add Psalm to your Laravel project via Composer:
composer require vimeo/psalm --dev
Initialize Psalm configuration by running:
vendor/bin/psalm --init
This generates a psalm.xml file in your project root.
First Run Analyze your entire project with default settings:
vendor/bin/psalm
For a quick check of a single file (e.g., app/Services/UserService.php):
vendor/bin/psalm --file app/Services/UserService.php
Integrate with Laravel’s CI
Add a Psalm step to your GitHub Actions (.github/workflows/psalm.yml):
- name: Run Psalm
run: vendor/bin/psalm --no-cache
Key Configurations
Update psalm.xml to include Laravel-specific paths:
<projectFiles>
<directory name="app" />
<directory name="config" />
<directory name="database/seeds" />
<directory name="routes" />
</projectFiles>
Pre-Commit Hooks
Use psalm with --report=github for PR-friendly output:
composer global require laravel/pint
composer global require dealerdirect/phpcodesniffer-composer-installer
composer require --dev dealerdirect/phpcodesniffer-composer-installer
Add to .git/hooks/pre-commit:
#!/bin/sh
vendor/bin/psalm --report=github --output-format=github
Laravel-Specific Annotations Annotate Eloquent models and services for better type inference:
use Psalm\Type\Union;
/**
* @psalm-import-type UserId from \App\Models\User
* @psalm-import-type UserData from \App\DTO\UserData
*/
class UserService {
/**
* @param UserId $userId
* @return UserData
*/
public function getUserData(string $userId): UserData { ... }
}
Security Analysis Enable taint analysis for input validation:
vendor/bin/psalm --issues=Security
Annotate controllers to mark tainted data:
/**
* @psalm-suppress MixedArgument
* @psalm-suppress MixedReturnStatement
*/
public function store(Request $request) {
$input = $request->input(); // Marked as tainted
$sanitized = $this->sanitize($input); // Should be pure
}
Plugin Integration
Use Psalm’s plugin API (v7+) to extend functionality. Example: Create a custom plugin for Laravel’s HasFactory trait:
// plugins/LaravelFactoryPlugin.php
namespace PsalmPlugin\Laravel;
use Psalm\Plugin\PluginEntryPointInterface;
class LaravelFactoryPlugin implements PluginEntryPointInterface {
public function __invoke(): void {
// Register stubs or custom rules
}
}
Register in psalm.xml:
<pluginClass>PsalmPlugin\Laravel\LaravelFactoryPlugin</pluginClass>
CI Feedback
Use --stats to track Psalm’s effectiveness:
vendor/bin/psalm --stats --output-format=json > psalm-stats.json
Parse JSON in CI for trend analysis.
False Positives with Laravel’s Magic Methods
Psalm may flag Arrayable, Jsonable, or Serializable as undefined. Add stubs or suppress:
/**
* @psalm-suppress MixedReturnType
*/
public function toArray(): array { ... }
Dynamic Properties in Eloquent
Psalm doesn’t recognize dynamic properties (e.g., $model->dynamicProp). Use @property annotations:
/**
* @property string $dynamicProp
*/
class DynamicModel extends Model { ... }
Closure Type Inference
Avoid callable for Laravel closures. Use specific types:
// Bad: Ambiguous
$callback = function () {};
// Good: Explicit
$callback = fn(): void => null;
JIT Performance Disable JIT for CI (enabled by default in v7+):
vendor/bin/psalm --no-jit
Plugin Conflicts
Ensure plugins are compatible with your Psalm version. Check composer.json for version constraints.
Isolate Issues
Use --file to narrow down problematic files:
vendor/bin/psalm --file app/Http/Controllers/UserController.php --issues=TypeError
Snippets for Context
Add --show-snippets to see code context:
vendor/bin/psalm --show-snippets --issues=UndefinedClass
Custom Error Levels
Adjust ERROR_LEVEL in psalm.xml to focus on critical issues:
<errorLevel>3</errorLevel> <!-- Only show errors/warnings -->
Autofix Common Issues
Use --alter to auto-fix MissingReturnType, MissingParamType, etc.:
vendor/bin/psalm --alter --issues=MissingReturnType,MissingParamType
Laravel-Specific Suppressions
Suppress issues for entire directories (e.g., vendor):
<suppressErrors>
<error name="MissingDocblockType">...</error>
<error name="MixedAssignment">...</error>
</suppressErrors>
Leverage @psalm-pure for Security
Mark pure functions to eliminate taint analysis false positives:
/**
* @psalm-pure
*/
function sanitizeInput(string $input): string { ... }
Use @psalm-mutation-free for Services
Improve mutability analysis in Laravel services:
/**
* @psalm-mutation-free
*/
class UserService { ... }
Custom Stubs for Third-Party Packages
Generate stubs for unsupported packages (e.g., spatie/laravel-permission):
vendor/bin/psalm --generate-stubs src
IDE Integration Configure PhpStorm to use Psalm’s inspection engine:
PHPSTORM env var to 1 for macOS/Linux:
export PHPSTORM=1
Performance Optimization Cache results in CI:
vendor/bin/psalm --cache-globals
Exclude node_modules and vendor from analysis:
<projectFiles>
<exclude-name>node_modules</exclude-name>
<exclude-name>vendor</exclude-name>
</projectFiles>
How can I help you explore Laravel packages today?