資料
prettier/plugin-phpの仕組みと、PHP code format - Slidev
"PHPerKaigi 2026 / fujitani sora / 登壇資料"
sorafujitani.github.io

PHPerKaigi 2026
PHPerKaigi(ペチパーカイギ)は、PHPer、つまり、現在PHPを使用している方、過去にPHPを使用していた方、これからPHPを使いたいと思っている方、そしてPHPが大好きな方たちが、技術的なノウハウとPHP愛を共有するためのイベントです。
PHPerKaigi 2026

本文
Prettier plugin-phpの仕組みと、PHP Code Formatというテーマで話します。よろしくお願いします。
QRにXのリンクがあって、資料へのリンクを固定ポストにしています。
手元でみたい方は読み取ってください。
fujitani soraといいます。
株式会社xxxでソフトウェアエンジニアをしています。
あとは、書いてあるような色々をやっています。
今日の内容に関係することで言うと、最近はできていませんがPrettierの開発をしていたり、rfmtというRubyで動くRust製Code Formatterのメンテナをやったりしています。
ざっくりの進み方ですが、まずはPrettierがどうPHPコードをフォーマットするのか、次に、Prettierがplugin systemをどう依存解決するのか、最後にPrettierとPHP固有の話をします。
かなり極端な例示ですが、こんなコードがレビューに上がってきたらどう感じるでしょうか。
このコードをどう整形するかについて同僚と意見が割れたら、その議論にどれほどのコストが費やされるでしょうか。
本来はこんな感じであって欲しいはずです。
コードをどう整形するかに関する課題を解決するためにコードフォーマッターというツールが存在します。
コードフォーマッターの一つにPrettierというOSSがあります。
JavaScriptで実装されており、複数言語に対応しています。
複数言語と言っても、Prettierが組み込みでサポートする言語と、組み込み以外でPrettierをサポートするためのプラグインシステムが存在し、後者はコミュニティによってメンテナンスされています。
このPrettierプラグインシステムの一つにplugin-phpが存在し、今日はこのツールがPHPコードをフォーマットするために何を実装しているか、
それを使ってPHPコードがどうフォーマットされるかを見ていきます。
まずは全体を確認します。
Prettierはどの言語を処理するかに関わらず、大きく3つの変換を通してアウトプットの文字列を作成します。
まず、インプットとなるPHPコードを文字列として入力し、php-parserを利用してASTを生成します。
次に、ASTを入力としてPrintAstToDocというPrettierに実装されているモジュールを利用してDocIRを生成します。
IRというのはIntermediate Representationの略で、中間表現という意味です。Prettierのフォーマット処理に適した構造に変換を行います。
具体的には後で取り上げます。
最後はDocIRを入力にPrintDocToStringの処理によってフォーマット済みの文字列を生成し、フォーマット後のコードがファイルに書き込まれます。
この変換順序の概要を把握していきましょう。
ステップ毎に見ていきます。
まず、Prettierは入力されたコードをパースしてASTを生成します。
ASTというのはAbstract Syntax Treeの略で、ソースコードを木構造で表現し、必要なデータを付与したものです。
スライドの例では関数宣言の命名とパラメータ、変数がechoされている構造がなんとなくわかると思います。
コードフォーマッターにおけるASTのポイントは、改行やインデントなどの情報が消えていることです。
実際にはパーサーから右下に記載されたようなJSオブジェクトが返却されます。
この中から重要なものを二つ抜粋します。
まずはkindノードです。
function、variablesなど、入力された式や文がどの種類であるかを表現します。
コードフォーマッターではkind毎に固有の処理が必要になるため、内部実装では大きなswitch文のコードで処理を分岐することに使用しています。
二つ目はlocノードです。locはlocationの略です。
locはその文字列がファイル上のどの位置に配置されているかを示します。
例えば、xyの軸を取る関数のグラフや、将棋盤なんかをイメージするとわかりやすいかもしれません。
一点に縦横で座標が決められ、数値情報から、具体的な場所を確定させることができます。
スライドを見てみましょう。
echoノードの構文が、0から11の文字列の間に収まっていることがわかります。
ポイントは、echoとnameの間のホワイトスペースも1文字としてカウントされていることです。
それによってコードフォーマッターが次のような実装を満たすことができます。
PHPのコードを見てみると、変数aとbのバリアブルズノードが記述されています。
パースすると、aに代入するノードは0からセミコロンの7まで、bに代入するノードは9から16までのloc情報であることを示しています。
そして、7と9の間の8のlocが間のホワイトスペースを表現します。
ASTからは改行やインデントの情報が削除されると話しましたが、Prettierは内部的にlocなどの情報を保持していて、後からそれを適用するアルゴリズムになっています。
これによって、元のコードから必要なインデントや改行の情報を維持したアウトプットを生成することができるようになっています。
で、ここまで聞いて勘がいい人は思っているかもしれませんが、普段書くコードから、一つ大事なものがASTでは抜け落ちています。
それがコメントアウトです。
コメントアウトというのはコードフォーマッターにおいて特別な扱いを受けます。
これは、ASTの変換に、コメントアウトの情報が含まれないためです。
どういうことかというと、例えばIDEで開発をする中で、コメントアウトしたコードからコードジャンプをできたことはないと思います。
コードジャンプというのはAST情報を利用しているのでそこに含まれないコメントアウトを操作対象にはできないわけです。
コードフォーマッターにおけるASTも同じで、コメントアウトをそのまま保持することができません。
なので、コメントアウト行のloc情報をPrettier内部のオブジェクトに保持しておいて、フォーマット後にloc情報を元に必要な場所に配置し直す、という処理が存在します。
次のコードを見てください。
単純な配列ですが、このコードはどうフォーマットされるべきでしょうか。
ワンライナーで書かれてもいいでしょうし、配列の括弧と中身の文字列で改行がされるケースもあると思います。
このコードをどうフォーマットするかを理解するために、printWidthオプションを知る必要があります。
設定ファイルからも決められる値で、一行にmaxで何文字まで並べることを許容するかを表現します。
Prettierでのデフォルトは80文字です。
先ほどの配列のフォーマットに戻りますが、おそらく回答は、配列に定義される文字列の長さによる、だと思います。
例えば、短い変数名であれば改行の必要はないでしょうし、変数名や文字列が長ければ改行されてほしい、というのがコードフォーマッターに求める自然な挙動だと思います。
これらのフォーマットの切り替えは、先ほど紹介したprintWidthに設定されている文字数に収まるかどうかによって確定されます。
ただし、実際のコードベースはそう単純ではありません。
配列はそれだけで定義されず、変数に代入されたり、関数のパラメータに取られたりすることがほとんどだと思います。
その時は、配列定義のみではなく、その行に記述されたコードと合わせてprintWidthに含まれるかを判定する必要があります。
後に記述される配列を改行するかどうかが、その前に記述されるコードの文字数次第になるということです。
これは、行の中で後に記述されるコードの改行判断に、その行の前に記述したコードの文字数が依存しているという制約になります。
さらに実際のコードではこのような構文がネストして記述されたりするので、深いネストに記述されたコードのフォーマット判断に一番大外のコードを参照する必要があるような状況になり、状態管理としても相当複雑です。
Prettierではこの課題を解決するために、DocIRという構造を利用し、2段階の変換によって安定したフォーマットを達成しています。
ASTをprintAstToDocに渡して、どう出力したいかと、どこで改行しても良いかを記述します。
printAstToDocがDocIRを生成し、DocIRに基づいて実際に改行やインデントなどを反映した文字列を生成するprintDocToStringを実行します。
DocIRという概念の具体についてみていきます。
ここではgroup, softline, indentを使用しています。
これは実際にPrettier内部で利用されているデータ構造です。
スライドを見て欲しいんですが、groupというのが、printWidthに収まるのであれば一行にしようとする範囲です。
配列自体を可能であれば一行で記述しようとすることは自然に理解できると思います。
中を見てみると、文字数が長くなって改行したい時は、配列の括弧の前後にあるsoftlineによる改行が宣言されます。
改行しなくていい場合はこのsoftlineは無視されるだけです。
softlineによって改行する場合は、その外側のindentも必要です。
配列を改行する際に括弧の位置と内部要素にインデントが入るのもよく見るフォーマットだと思います。
これらのフォーマット判断を用いて、より大きなコードベースでも統一的な仕組みでフォーマットが実行できます。
少し実践的な例に近づけたコードを見てみます。
Classからの返却値を変数に代入するコードですが、同じように可能であればgroupで一行にしてprintWidthを越えればsoftlineの位置で改行、indentの位置でインデントすることになります。
DocIRの仕組みのポイントとして、それ自体が各plugin systemの中間表現になり、それ以降の実装を共通化できる点があります。
plugin systemのコードベースには言語ごとのPrintAstToDocによるDocIR生成の実装がありますが、DocIRの構造は同じインターフェースであるためにそれ以降のPrintDocToStringの実装はPrettier coreが所有する実装の一つだけで済みます。
これにより、plugin systemが今後増えていっても、実際のformat処理のコードを言語ごとに増やす必要がないメリットにつながっています。
少し内側の話として、ここまでのDocIRによる変換の話は、2003年に公開された”A Prettier Printer”という論文にまとめられています。
PrettierのDocIRもこの内容を踏襲した仕組みになっており、過去のコミットログで確認することができます。
次は、Prettier側がplugin-phpのコードとどう連携しているのかについてみていきます。
単純な仕組みなので簡単にですが。
Prettierのplugin systemは、Prettier coreが期待する5つのインターフェースを満たすことで動作する仕組みになっています。
主要なものを紹介します。
まず、Prettierは「どのplugin systemを利用するか」の判断に、フォーマット対象のファイルの拡張子を参照しています。
ASTを生成するためのパーサの選択も実行時に差し替えることができます。
これも拡張子によって判定されていて、PHPでは基本的にphp-parserを利用することになります。
後述しますが、PHPを使っている人が普段使うphp-parserとは少し違います。
最後に、ユーザー固有のオプションの解決を行います。
例を挙げておくと、インデントするときにspaceにするかtabにするか、などはよく意見が分かれる設定項目だと思います。
.prettierrcファイルで設定することができ、何もしなければデフォルトオプションが適用されます。
ここまで、Prettier plugin-phpがPHPコードをフォーマットするまでの仕組みと依存解決について話してきましたが、PHPネイティブ対応ではないplugin故の課題を紹介します。
PHPにはPER Coding Styleという、コミュニティによってメンテナンスされるコード規約が存在します。
例えば、JavaScriptでは関数宣言と同じ行にブロックスコープの括弧を置くことが多いですが、PHPでは関数宣言に改行して括弧を置くことも、PERに明記されているルールの一つです。
しかし、Prettier plugin-phpでは、このPERに準拠しないフォーマット動作が存在しています。
例えば、連続する論理演算子の配置においてPERでは条件の前に置くことが明記されていますが、Prettierを使うとこれが条件の後ろに配置されます。
これはJavaScriptで実装されている故の実装依存の問題であり、このようなPERに基づいたケースに対応しきれていないことは、Prettier plugin-phpのREADMEで明記されています。
もう一つ面白い話があって、plugin-phpがAST生成に利用するパーサはnikic/PHP-Parserではなく、それを参考にJSで再実装したglayzzle/php-parserに依存しています。
この意思決定によって実装面で嬉しいこともありますが、新しいPHP構文のサポートが遅れることであったり、再実装故のバグも存在します。
このケースは自分がPrettierの開発をしていたときに踏んでメンテナにissueを作ってもらったやつなのですが、インスタンス化のメソッドチェーンでglayzzle/php-parserのバージョンによって生成されるASTが変化し、それに起因してフォーマット後のコードの意味が変わってしまう問題などがあります。
再実装されたパーサの品質に、フォーマッター自身の品質も影響されるという依存関係につながっているとは思います。
まとめると、Prettier plugin-phpは、拡張子によって依存が解決され、
3段階の変換によってフォーマットを行います。
その過程でPHP由来の課題や制約に対応していると言う話が伝わればいいです。
最後に、今日はコードフォーマッターの話をしましたが、
これに類するtoolchainの開発は去年や今年に入っても、かなり流れが変わってきているように感じます。
Agentを使う上でも、依存するツールの高速さや使い勝手は切大だったりすると思います。
PHPでもMagoのような高速なtoolchainが出てきていたりしますね。
ぜひこの機会に、toolchainに何を使うかやその設定を見直したり、内部実装を見てみたりしてくれると嬉しいです。
Note
PER に従わない部分もある
const right = shouldInline
? [node.type, " ", print("right")]
: [node.type, line, print("right")];
node.type(演算子 &&)が print("right")(右辺)の前に置かれています。つまり [演算子, 改行, 右辺] という並びで、改行が入ると 演算子が前の行の末尾に残る構造です。
これは Prettier の JS 側のバイナリ式の処理と同じロジックをそのまま流用しているためです。JS の Prettier も同じく演算子を行末に置きます。plugin-php は JS の prettier コアから
printBinaryExpression のパターンを借りてきたので、PER の「行頭に演算子」というルールとは逆になっています。
つまり JS の慣習に合わせた実装がそのまま PHP に持ち込まれているのが理由
- Intermediate Representation インターミディエイト・リプレゼンテーション
- nikic/PHP-Parser: 「ニキック」(nikic はセルビア系の開発者 Nikita Popov のハンドルネーム)
- glayzzle/php-parser: 「グレイズル」