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

Level Up your PHP Code Style
01 Aug 2024
|
4 min read

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

1return romanzipp\Fixer\Config::make()
2    ->in(__DIR__)
3    ->preset(
4        new romanzipp\Fixer\Presets\PrettyPHP()
5    )
6    ->out();

To run PHP-CS-Fixer, just execute...

1php-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

 1parameters:
 2  level: 2
 3  phpVersion: 80300
 4  paths:
 5    - app
 6    - routes
 7  excludePaths:
 8    - tests
 9  ignoreErrors:
10    - '#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...

1phpstan

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.

PHPStan errors without IDE completion

With added PHPDoc blocks, declared relationships will get the according accessor and provide a _count attribute. And much more!

Intelligent IDE completions with Laravel-Model-Doc

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:

  1. Specify the table name explicitly protected $table = 'users';

  2. Add return types to your relationship methods public function teams(): HasMany

  3. Add return types to your accessor methods

1php artisan model-doc:generate

Before

 1use Illuminate\Database\Eloquent\Builder;
 2use Illuminate\Database\Eloquent\Factories\HasFactory;
 3use Illuminate\Database\Eloquent\Model;
 4
 5class MyUser extends Model
 6{
 7    use HasFactory;
 8
 9    protected $table = 'users';
10
11    protected $casts = [
12        'children' => 'array',
13    ];
14
15    public function teams(): HasMany
16    {
17        return $this->hasMany(Team::class);
18    }
19
20    public function scopeWhereTeamName(Builder $builder, string $name)
21    {
22        $builder->where('name', $name);
23    }
24
25    public function getPrettyTitleAttribute(): string
26    {
27        return ucfirst($this->title);
28    }
29    
30    protected static function newFactory()
31    {
32        return new \Database\Factoies\MyUserFactory();
33    }
34}

After

 1use Illuminate\Database\Eloquent\Builder;
 2use Illuminate\Database\Eloquent\Factories\HasFactory;
 3use Illuminate\Database\Eloquent\Model;
 4
 5/**
 6 * @property string $id
 7 * @property string $title
 8 * @property string $pretty_title
 9 * @property string|null $icon
10 * @property int $order
11 * @property bool $enabled
12 * @property array $children
13 * @property \Illuminate\Support\Carbon|null $created_at
14 * @property \Illuminate\Support\Carbon|null $updated_at
15 *
16 * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Team[] $teams
17 * @property int|null $teams_count
18 *
19 * @method static \Illuminate\Database\Eloquent\Builder whereTeamName(string $name)
20 * 
21 * @method static \Database\Factoies\MyUserFactory<self> factory($count = null, $state = [])
22 */
23class MyUser extends Model
24{
25    // same as above...
26}

See on how to implement Laravel-Model-Doc on GitHub


Read more...