🥊

Lefthook で PHPStan と PHP-CS-Fixer と PHPUnit を使う

2025/01/19に公開

はじめに

PHP の開発でよく使う以下のツールを Lefthook で動かしてみたので備忘録。

環境に依存したくなかったので Docker を使って実行してみる。

前提

  • 以下がインストールされていること。
  • lefthook.yml は設定済み。

セットアップ

あらかじめ lefthook.yml があるディレクトリ内で以下を実行しておく必要がある。

% lefthook install

動作確認をしたディレクトリ構成は以下。

├── .git
├── .php-cs-fixer.dist.php
├── lefthook.yml
├── phpstan.dist.neon
├── phpunit.dist.xml
├── src
│   └── sample.php
└── tests
    └── SampleTest.php

Lefthook

  • 辞書順にコマンドが実行されるのでコマンド名の先頭に数字をつけておく
  • tags - コマンド指定用のタグ
  • {staged_files} - ステージングされたファイルを指定
  • stage_fixed: true - 修正を自動でステージングする
lefthook.yml
pre-commit:
  commands:
    1_phpstan:
      tags: [dev, scan]
      glob: "src/*.php"
      run: docker run -it --rm -v $(pwd):/app 
        ghcr.io/phpstan/phpstan:2-php8.3 analyse {staged_files}
    2_php-cs-fixer:
      tags: [dev, format]
      glob: "src/*.php"
      stage_fixed: true
      run: docker run -it --rm -v $(pwd):/code 
        ghcr.io/php-cs-fixer/php-cs-fixer:3-php8.3 fix {staged_files}
    3_phpunit: 
      tags: [dev, test]
      glob: "{tests,src}/*.php"
      run: docker run -it --rm -v $(pwd):/app 
        ghcr.io/jitesoft/phpunit:8.3

PHPStan

phpstan.neon
parameters:
    level: 7

PHP-CS-Fixer

.php-cs-fixer.dist.php
<?php
declare(strict_types=1);

return (new PhpCsFixer\Config())
    // ↓並列実行できる場合の設定
    ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
    ->setRiskyAllowed(true)
    ->setRules([
        '@PSR12' => true,
        'strict_param' => true,
        'return_assignment' => true,
        'array_syntax' => ['syntax' => 'short'],
    ])
;

PHPUnit

phpunit.dist.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    failOnRisky="true"
    failOnWarning="true"
    colors="true"
>
    <testsuites>
        <testsuite name="default">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>

動作確認用の PHP のコード

src/sample.php
<?php
declare(strict_types=1);
function hoge(int $a, int $b){
if(in_array($b, [1, 2, 3, 4, 5])){
return $a + $b;
}
return $a;
}

echo hoge(5, 10) . PHP_EOL;
echo hoge('5', 10) . PHP_EOL;
tests/SampleTest.php
<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class SampleTest extends TestCase
{
    public function testSample(): void
    {
        $this->assertSame('3', 1 + 2);
    }
}

動作確認

% git add src/sample.php
% git commit
╭───────────────────────────────────────╮
│ 🥊 lefthook v1.10.8  hook: pre-commit │
╰───────────────────────────────────────╯
┃  1_phpstan ❯

Note: Using configuration file /app/phpstan.dist.neon.
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ -------------------------------------------------------------
  Line   sample.php
 ------ -------------------------------------------------------------
  4      Function hoge() has no return type specified.
         🪪  missingType.return
  13     Parameter #1 $a of function hoge expects int, string given.
         🪪  argument.type
 ------ -------------------------------------------------------------



 [ERROR] Found 2 errors



┃  2_php-cs-fixer ❯

PHP CS Fixer 3.66.0 Persian Successor by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.7
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from "/code/.php-cs-fixer.dist.php".
Using cache file ".php-cs-fixer.cache".
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

   1) src/sample.php

Fixed 1 of 1 files in 0.005 seconds, 16.00 MB memory used

┃  3_phpunit ❯

PHPUnit 11.5.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.16
Configuration: /app/phpunit.dist.xml

F                                                                   1 / 1 (100%)

Time: 00:00.002, Memory: 25.36 MB

There was 1 failure:

1) SampleTest::testSample
Failed asserting that 3 is identical to '3'.

/app/tests/SampleTest.php:10

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


  ────────────────────────────────────
summary: (done in 1.55 seconds)
✔️ 2_php-cs-fixer
🥊 1_phpstan
🥊 3_phpunit

fix された PHP ファイル。

<?php

declare(strict_types=1);
function hoge(int $a, int $b)
{
    if (in_array($b, [1, 2, 3, 4, 5], true)) {
        return $a + $b;
    }
    return $a;
}

echo hoge(5, 10) . PHP_EOL;
echo hoge('5', 10) . PHP_EOL;

特定のコマンドをスキップする

LEFTHOOK_EXCLUDE 変数にコマンド名やタグを指定することで特定のコマンドをスキップできる。

コマンド名を指定した場合。

% LEFTHOOK_EXCLUDE=phpunit git commit  

タグを指定した場合。

% LEFTHOOK_EXCLUDE=dev git commit  

所感

  • 絵文字が右手 😅
  • lefthook install の実行を忘れそうなので工夫が必要
  • {staged_files} 指定は良き
  • PHP-CS-Fixer で修正分を勝手にステージングされるのは微妙だった
    • 修正を確認してからステージングしたいところ
  • PHPUnit はエクステンションとかの依存もあるので開発用のコンテナで実行した方が良さそう
    • それを言い出したら他のツールも同様か
  • コミットのたびに全テスト実行するのはしんどいので PHPUnit は手動でもよいかな
    • CI サーバでの担保は必要

おまけ

並列実行の設定をしないまま PHP-CS-Fixer を実行すると以下のメッセージが表示される。

You can enable parallel runner and speed up the analysis! Please see usage docs for more information.

で、並列実行を設定すると以下のメッセージが表示される。

Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!

いや、まぁそうなんだろうけど、なんか理不尽。。。

環境

% sw_vers
ProductName:            macOS
ProductVersion:         15.2
BuildVersion:           24C101

% docker --version
Docker version 27.4.0, build bde2b89

% lefthook version
1.10.8

Discussion