Level Up your PHP Code Style
A small suite of tools and linters I've learned to love over the time when working with PHP and Laravel
Roman Zipp, August 1st, 2024
1. PHP CS Fixer
The PHP Coding Standards Fixer (PHP CS Fixer) tool fixes your code to follow standards; whether you want to follow PHP coding standards as defined in the PSR-1, PSR-2, etc., or other community driven ones like the Symfony one. You can also define your (team's) style through configuration.
Why?
Especially when working in teams, different syntax formatting can lead to merge conflicts resulting in frustration and possibly bugs. Thus, a standardized syntax is a key element in collaborating with others. All of my projects require the same coding style.
Configuration Management
Over the time I've built a preferred style config for my own projects which I've open sourced at romanzipp/PHP-CS-Fixer-Config. This repository contains some curated PHP-CS-Fixer rules with custom presets. Of course, presets can be extended and rules can be overridden in each consuming project.
.php-cs-fixer.dist.php
return romanzipp\Fixer\Config::make() ->in(__DIR__) ->preset( new romanzipp\Fixer\Presets\PrettyPHP() ) ->out();
To run PHP-CS-Fixer, just execute...
php-cs-fixer fix
Integrate with your CI
One argument of the PHP-CS-Fixer cli we can make use of, is --dry
option. This will not modify your code and only run the validation which returns an error code if the optimal code format missmatches the source. This will allow your to integrate a syntax linting step into your CI, failing builds if the format is off.
Tip: Add a hotkey to your IDE such as CMD + SHIFT + C
to automatically format your code.
2. PHPStan
PHPStan is a static linter for your PHP projects. As simple as that.
Configuration is done through YAML files, which contain the PHP language level, strictness and other parameters. This is an example phpstan.neon.dist
parameters: level: 2 phpVersion: 80300 paths: - app - routes excludePaths: - tests ignoreErrors: - '#Call to an undefined method Illuminate\\Database\\(Eloquent\\Relations|Query)\\[A-Za-z]+::(with|count|where|orderBy|whereHas|orWhereHas|whereDoesntHave|withWhereHas|whereIn|sum|select|find|has)\(\)#'
To run PHPStan, just execute...
phpstan
Integrate with PHPStorm
PHPStan can also perform linting and show issues directly inline in your IDE, such as PHPStorm. See the official JetBrains PHPStorm guide on how to integrate with PHPStan.
3. Laravel Model Doc - GitHub
Laravel has catapulted PHP to a new level and the language is more popular than ever. But one thing that has always driven me crazy is the lack of typing which leads in developers needing to rely on IDE plugins to get helpful code suggestions.
This is why I've created the Laravel-Model-Doc package, which automatically generates PHPDoc comments for all of your Models and writes them to the class file.
The Problem
Laravel provides many handy features, such as accessing loaded relationships via a magic accessor. Unfortunately, static linters will see this as a syntax error since the attribute has not been explicitly declated. Additionally, you will not get any autocomplete support from your IDE.
With added PHPDoc blocks, declared relationships will get the according accessor and provide a _count
attribute. And much more!
How
Laravel-Model-Doc takes many sources into consideration when generating doc blocks, such as...
Model Relationships
Model Factories
The Database structure (attributes, fields)
Custom accessor methods
Query scope methods
In the latest update, the package will also add generics annotations such as Collection<ModelClass>
.
Let's see it in Action
One tiny caveat before using the package: You will need to make some adjustments to your models, which - in my opinion - additionally provide more safety. These are:
Specify the table name explicitly
protected $table = 'users';
Add return types to your relationship methods
public function teams(): HasMany
Add return types to your accessor methods
php artisan model-doc:generate
Before
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class MyUser extends Model { use HasFactory; protected $table = 'users'; protected $casts = [ 'children' => 'array', ]; public function teams(): HasMany { return $this->hasMany(Team::class); } public function scopeWhereTeamName(Builder $builder, string $name) { $builder->where('name', $name); } public function getPrettyTitleAttribute(): string { return ucfirst($this->title); } protected static function newFactory() { return new \Database\Factoies\MyUserFactory(); } }
After
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; /** * @property string $id * @property string $title * @property string $pretty_title * @property string|null $icon * @property int $order * @property bool $enabled * @property array $children * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Team[] $teams * @property int|null $teams_count * * @method static \Illuminate\Database\Eloquent\Builder whereTeamName(string $name) * * @method static \Database\Factoies\MyUserFactory<self> factory($count = null, $state = []) */ class MyUser extends Model { // same as above... }