Level Up your PHP Code Style

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.

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

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...
}

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