こんにちは。開発支援チームでpixivのコーディング環境の向上をしているyosatakです。
pixivではPHPStanを活用して、スクリプト言語であるPHPのコーディング上のミスをデプロイ前に検出しています。
PHPStanは開発者にエディタを強制したりせずに静的な型検査ができるだけではなく、入力のアサーション関数などに対してPHPStan拡張を書くことでリクエストパラメータなどの不確定な入力に厳密に型をつけ、PHPで安全にコーディングすることができるようになります。
それでも、10年以上メンテナンスされつづけているpixivのソースコードに型を付けていくのは容易ではありません。
PHPStanで特定のファイルの解析を掛けたい場合は、autoloadするファイルをbootstrapFilesに指定されたphpstan.neon(.dist)が設置されたディレクトリ以下で
./vendor/bin/phpstan analyse {解析対象のファイル}
とすることで可能です。
pixivでは編集したファイルと参照されているメソッドが含まれているファイルをCI上で解析しています。
CIで掛かるPHPStanが警告を出してきた箇所に対応する最良の方法を開発者同士で議論することも多くありますが、GitLabにpushされたコードとCIの出力を見て変更の提案をSlackやmerge requestのコメントで行なうために警告を再現する環境を作るのには作業中の変更をstashしてcheckoutするなど作業コストがかかり、簡単に提案できる方法があることで気軽に議論できるようになります。
phpstan.orgではブラウザ上でソースコードを入力してPHPStanの検査を試せるplaygroundが用意されています。簡単なコードはここに入力すれば最新版のPHPStanでどのように型を解釈されるかをチェックできますが、業務のアプリケーションコードを確認するにはセキュリティや情報管理上の問題がある上に、ライブラリなど外部で定義されたクラスをコピペするのは現実的ではなく、とても不便です。
そこで、実際にアプリケーションのコードをロードして動作するPHPStanのPlaygroundをLaravel Livewireを用いて素早く作りました。
Laravel Livewireについて
Laravel LivewireはダイナミックなフロントエンドUIをPHPで書くことができるフルスタックフレームワークです。
リアルタイム性のある差分レンダリングをするWebアプリケーションを作成するためには、フロントエンドのJavaScriptフレームワークを用いてフロントエンドを開発し、REST APIを設計することでバックエンドと繋ぎこむことが一般的でした。
Laravel Livewireを利用すると、サーバーサイドでステートを保持することができ、REST APIを設計することなく、バックエンドの言語だけで部分的に描画を更新するようなダイナミックなアプリケーション開発をすることができます。
ElixirのPhoenix LiveViewを始めとして、最近ではRuby on RailsのHotwireが発表されるなど、似たコンセプトのライブラリが活発に開発されています。使い慣れた言語でぜひ利用してみてください。
playgroundの実装
Laravel 8の新規プロジェクトは以下のようにPHPとComposerが導入されたコンピュータで以下の様に簡単に作成することができます。
composer create-project laravel/laravel
pixivの開発はPHPがネイティブにインストールされたイントラ内の共有開発サーバーで行なわれており、Apache HTTP ServerのVirtualDocumentRootが利用できるため、一瞬で社内向けにプロジェクトを展開することが可能です。Dockerを用いて開発することも可能ですが、今回はDockerを利用せずに共有開発サーバー上でプロジェクトを動かすことにしました。
開発環境ができたら、Livewireをインストールし、コンポーネントのボイラープレートを展開しましょう。
PHPコードを検査するために別プロセスでPHPStanを起動するため、今回はsymfony/processをインストールします。プロセス起動はexec()
関数やproc_open()
関数を使っても可能ですが、細かい設定やハンドリングのためのワークアラウンドを減らせるのでsymfony/processを使うと便利です。
今回はphpstan-checker
というLivewireのコンポーネントを作成します。
composer require livewire/livewire composer require symfony/process php artisan make:livewire phpstan-checker
さあ、コンポーネントを生成したところで、テンプレートをつくっていきます。
Livewireの公式ドキュメントを参考にFull page componentを作っていきましょう。
Livewireはコンポーネント毎にサーバーサイドでレンダリングをするため、ダッシュボードのような複数の情報を一箇所に表示するようなケースではコンポーネントをテンプレートエンジンで展開していった方が良いですが、今回は単機能なページを生成するので、Full page componentを使います。
マニュアルに則り、ルートのlayout
ファイルとルーティング定義をそれぞれresources/views/layouts/app.blade.php
とroutes/web.php
に書いていきます。
<!-- resources/views/layouts/app.blade.php --> <html> <head> @livewireStyles <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"> </head> <body> {{ $slot }} @livewireScripts </body>
ブラウザのデフォルトCSSだと寂しいので、no class CSSフレームワークであるwater.cssを適用してみました。
water.cssはBootstrapのようにHTML要素に専用のclassを指定せずとも簡単なCSSが当たるので、僕はラピッドプロトタイピングなどに良く利用します。
ルーティング定義 routes/web.php
は次のようにします。
Route::redirect('/', '/' . \Str::random(20)); Route::get('/{code_id}', \App\Http\Livewire\PhpstanChecker::class);
今回は /
にアクセスすると新規に20文字のcode_idを発行して /{code_id}
に転送されるようにしました。
ここまで来ればURLにアクセスすることで先程展開したコンポーネントが表示されるようになりました。
コンポーネントは以下のように実装しましょう。
<!-- resources/views/livewire/phpstan-checker.blade.php --> <div> <textarea rows="60" wire:model.debounce.750ms="analyseCode"></textarea> <div> @if (isset($result['files'])) @foreach ($result['files'] as $file => $error) @foreach ($error['messages'] as $message) <p> {{ $message['line'] }} :{{ $message['message'] }} </p> @endforeach @endforeach @endif </div> </div>
// app/Http/Livewire/PhpstanChecker.php <?php namespace App\Http\Livewire; use Livewire\Component; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; class PhpstanChecker extends Component { public $analyseCode; public $result; private $codeId; private $codePath; public function mount($code_id): void { $this->codePath = realpath(storage_path(‘tmp’)); if (preg_match('/\A[a-zA-Z0-9]+\z/',$code_id)) { $this->codeId = '/' . $code_id; } else { redirect()->to('/' . \Str::random(20)); } if (file_exists($this->codePath . $this->codeId)) { $this->analyseCode = file_get_contents($this->codePath . $this->codeId); } else { $this->analyseCode = ''; } $this->analyse(); } public function updated($name, $value): void { file_put_contents($this->codePath . $this->codeId, $value); $this->analyse(); } public function render() { return view('livewire.phpstan-checker'); } private function analyse() { $process = new Process(['./vendor/bin/phpstan', 'analyse', '--error-format=json', $this->codePath . $this->codeId]); $process->setWorkingDirectory(base_path(‘../project_path’)); //ここにproject-rootから解析対象のファイルへの相対Path or絶対Pathを入れる $process->run(); $this->result = json_decode($process->getOutput(), true); } }
app/Http/Livewire/PhpstanChecker.php
の下から5行目にロードしたいプロジェクトのURLを指定することで、そのプロジェクトでphpstanのコマンドを発行したのと同じ出力を得ることができます。
Livewireでは、JSフロントエンドフレームワークのコンポーネントの様にプロパティとテンプレートエンジンで指定したmodelが同期されます。
textarea
に入力され、プロパティが更新される度にPhpstanChecker::updated()
メソッドが呼ばれるので、そこで解析を実行し、結果をフロントエンドに返却しています。
完成
4つのコマンドと4つのファイル修正でプロジェクトの中でリアルタイムに解析を掛けられるplaygroundができました。
pixivでは値の型を限定するために独自のアサーションメソッドを用意しており、phpstan/phpstan-webmozart-assertを参考にしたPHPStan拡張を書いてあるため、入力値の型が曖昧でも、assert後には型が絞り込まれるようになっています。
上のスクリーンショットでは、様々な値を取れる関数pixiv()
の引数$param
の型を関数内で絞り込んでいるため、11行目のif
文が常にfalse
になり、意味の無いコードが書かれている旨の警告が表示されています。
同様に関数内部では正の整数として型を絞り込んでいるので、if
文で比較される際に$param
が-1
になることは絶対に無い旨の警告が表示されています。このようにPHPStanは単なるデータ型に留まらず、数字の値域なども含めて静的に検査できます。
CodeMirrorの導入
これまで、Livewireを用いたPHPStan Playgroundの実装について説明してきました。手間を掛けずにこのようなシステムを開発できるLivewireは強力なツールでしょう。
phpstan.orgではブラウザ上のエディタコンポーネントとしてCodeMirrorが組みこまれたPlaygroundが利用できるため、完成後に今回作成したplaygroundにも組み込んでみました。
こちらに関しては後日、リポジトリの準備が整い次第公開いたします。
PHPerkaigi2021が開催されます
弊社ではPHPStanを用いた解析を行ない大規模なプロジェクトのリファクタリングを行なっています。
3月26日〜28日 に開催されるPHPerkaigiではPHPStanの活用術などを紹介する予定です。 PHPerKaigiではpixivから合計3名のエンジニアが以下の演題を発表する予定です。
これらのセッションはピクシブのエンジニアが業務内外で直面した課題やそれを解決する為のノウハウを公開する充実したセッションとなっております。
PHPerの皆様もそうではないWebエンジニアの皆様も楽しめる内容となっていますので、是非ご視聴ください。