🦁

PHP-CS-Fixerはどう動く?「fix」コマンドのエントリーポイントをコードで追う

に公開

PHP-CS-Fixerを“使う側”でも“作る側(OSSコントリビュート)”でも、最初に理解しておきたいのが「どこから処理が始まり、どこでFixerが呼ばれるのか」という流れです。
この記事では fix コマンドを題材に、autoload → Console Application → FixCommand → Runner → Fixer のつながりを、実際のコードをもとに解説します。

全体像

[CLI スクリプト php-cs-fixer]
   → vendor/autoload.php(Composer autoload 起動)
      → PhpCsFixer\Console\Application(コマンド登録)
         → PhpCsFixer\Console\Command\FixCommand(設定読込・実行準備)
            → PhpCsFixer\Runner\Runner(ファイル走査・Fixer適用)
               → PhpCsFixer\Fixer\...(applyFix 実体)

1) 入口:CLI スクリプト php-cs-fixer

場所:リポジトリ直下の実行ファイル。Laravelでいう public/index.php に相当します。
やっていることは「Composerのオートローダを探して読み込み、Console Applicationを起動する」だけです。

#!/usr/bin/env php
<?php

$autoloadPaths = [
    __DIR__.'/vendor/autoload.php',
    __DIR__.'/../../autoload.php',
];

foreach ($autoloadPaths as $path) {
    if (file_exists($path)) {
        require_once $path;
        break;
    }
}

use PhpCsFixer\Console\Application;

$application = new Application();
$application->run();

ここが最初のエントリーポイントです。
vendor/autoload.php が見つからない場合、「Unable to locate autoload.php」が発生します。

2) Composer autoload:PSR-4 によるクラス解決

composer.json の設定により、PhpCsFixer\ 名前空間のクラスは src/ ディレクトリにマッピングされます。

"autoload": {
  "psr-4": { "PhpCsFixer\\": "src/" }
},
"autoload-dev": {
  "psr-4": { "PhpCsFixer\\Tests\\": "tests/" }
}

Composerは vendor/autoload.php を通してこのマッピングを登録し、クラスを自動的に読み込めるようにします。
開発者として本体を直接動かす場合は、このリポジトリ内で composer installcomposer update を実行し、vendor/autoload.php を生成しておく必要があります。

3) Console Application:PhpCsFixer\Console\Application

Symfony Console をベースにしたアプリケーションの本体です。
ここで fix, list, help などのコマンドが登録されます。

class Application extends Symfony\Component\Console\Application
{
    public function __construct()
    {
        parent::__construct('PHP CS Fixer', self::VERSION);
        $this->add(new Command\FixCommand());
        // 他にもコマンドが追加される
    }
}

Application::run() が呼ばれると、ユーザの入力に応じて該当コマンドの execute() が実行されます。

4) FixCommand:PhpCsFixer\Console\Command\FixCommand

fix サブコマンドの本体です。設定の読込からRunnerの起動までをまとめています。

protected function execute(InputInterface $input, OutputInterface $output)
{
    $config = $this->resolveConfig($input);

    $factory = new FixerFactory();
    $factory->registerBuiltInFixers();
    $fixers = $factory->useRuleSet($config->getRules())->getFixers();

    $runner = new Runner(
        $config->getFinder(),
        $fixers,
        $config->getDiffer(),
        $config->getCacheManager(),
        $input->getOption('dry-run')
    );

    $changed = $runner->fix();
    return $changed ? self::FAILURE : self::SUCCESS;
}

ここまでで「どのファイルにどのルールをどう適用するか」が決まります。
FixerFactory は内部で組み込みFixerを登録し、設定ファイル(.php-cs-fixer.dist.php)に基づいて使うFixer群を抽出します。

5) Runner:PhpCsFixer\Runner\Runner

Runnerは、実際のファイル処理とFixer適用を担当する層です。

public function fix(): bool
{
    $hasChanges = false;

    foreach ($this->finder as $fileInfo) {
        $code   = file_get_contents($fileInfo->getPathname());
        $tokens = Tokens::fromCode($code);

        foreach ($this->fixers as $fixer) {
            if ($fixer->isCandidate($tokens)) {
                $fixer->applyFix($fileInfo, $tokens);
            }
        }

        if ($tokens->isChanged()) {
            $hasChanges = true;
            $this->writeBack($fileInfo, $tokens);
        }
    }

    return $hasChanges;
}

Finderで対象ファイルを列挙し、Tokens クラスでトークン化。
各Fixerの applyFix() を順に適用して、変更があればファイルを書き戻します。

6) Fixer:PhpCsFixer\Fixer\...

Fixerは、実際の整形処理を行う小さな単位のクラスです。
すべて FixerInterface を実装し、主要メソッドは applyFix() です。

final class PhpdocAlignFixer implements FixerInterface
{
    public function isCandidate(Tokens $tokens): bool
    {
        // PHPDocコメントが含まれるかなど、軽い適用条件チェック
    }

    public function applyFix(SplFileInfo $file, Tokens $tokens): void
    {
        // トークンを走査し、@paramなどを整列
    }
}

入力は Tokens、出力も Tokens
Fixerは「単一責務(1つのスタイル問題を直す)」で作られ、Runnerによって順に適用されます。

利用者と開発者のautoloadの違い

  • 利用者は ./vendor/bin/php-cs-fixer(Composer生成のラッパ)を実行するだけでOK。
    ラッパ側が自動で 自分のプロジェクトの vendor/autoload.php を読み込みます。
  • 開発者が本体リポを直接実行する場合は、このリポジトリの vendor/autoload.php を用意する必要があります。
    つまり composer install をしないとCLIは起動しません。

autoloadの読み込み先が「プロジェクトか本体リポか」で異なる点を混同しないのが重要です。

全体まとめ

レイヤ ファイル 役割
CLI php-cs-fixer 実行スクリプト。autoloadを読み込み、Applicationを起動
autoload vendor/autoload.php PSR-4でクラスを自動解決
Application src/Console/Application.php fix/list等のCLIコマンド登録
FixCommand src/Console/Command/FixCommand.php 設定読込・Runner起動
Runner src/Runner/Runner.php ファイル列挙・Fixer適用
Fixer src/Fixer/... 整形ロジック(applyFix実装)

まとめ

  • エントリーポイントは php-cs-fixer スクリプト。autoloadを経て Application → FixCommand → Runner → Fixer と流れる。
  • fix コマンドの実体は FixCommand::execute() 内で、FixerFactoryとRunnerを中心に動く。
  • Fixerはすべて FixerInterface を実装し、トークン列を直接変換する。
  • autoloadの文脈が「利用者」と「開発者」で異なる点に注意。

Discussion