entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

プロジェクト内のライブラリをロードしつつ、ブラウザで動作するPHPStan Playground

こんにちは。開発支援チームでpixivのコーディング環境の向上をしているyosatakです。

pixivではPHPStanを活用して、スクリプト言語であるPHPのコーディング上のミスをデプロイ前に検出しています。

inside.pixiv.blog

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

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を作っていきましょう。

laravel-livewire.com

Livewireはコンポーネント毎にサーバーサイドでレンダリングをするため、ダッシュボードのような複数の情報を一箇所に表示するようなケースではコンポーネントをテンプレートエンジンで展開していった方が良いですが、今回は単機能なページを生成するので、Full page componentを使います。

マニュアルに則り、ルートのlayoutファイルとルーティング定義をそれぞれresources/views/layouts/app.blade.phproutes/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を適用してみました。

watercss.kognise.dev

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後には型が絞り込まれるようになっています。

f:id:pxvpxv:20210303232411p:plain

上のスクリーンショットでは、様々な値を取れる関数pixiv()の引数$paramの型を関数内で絞り込んでいるため、11行目のif文が常にfalseになり、意味の無いコードが書かれている旨の警告が表示されています。

同様に関数内部では正の整数として型を絞り込んでいるので、if文で比較される際に$param-1になることは絶対に無い旨の警告が表示されています。このようにPHPStanは単なるデータ型に留まらず、数字の値域なども含めて静的に検査できます。

CodeMirrorの導入

これまで、Livewireを用いたPHPStan Playgroundの実装について説明してきました。手間を掛けずにこのようなシステムを開発できるLivewireは強力なツールでしょう。

phpstan.orgではブラウザ上のエディタコンポーネントとしてCodeMirrorが組みこまれたPlaygroundが利用できるため、完成後に今回作成したplaygroundにも組み込んでみました。

f:id:pxvpxv:20210303232749p:plain

こちらに関しては後日、リポジトリの準備が整い次第公開いたします。

PHPerkaigi2021が開催されます

弊社ではPHPStanを用いた解析を行ない大規模なプロジェクトのリファクタリングを行なっています。

3月26日〜28日 に開催されるPHPerkaigiではPHPStanの活用術などを紹介する予定です。 PHPerKaigiではpixivから合計3名のエンジニアが以下の演題を発表する予定です。

phperkaigi.jp

fortee.jp fortee.jp fortee.jp

これらのセッションはピクシブのエンジニアが業務内外で直面した課題やそれを解決する為のノウハウを公開する充実したセッションとなっております。

PHPerの皆様もそうではないWebエンジニアの皆様も楽しめる内容となっていますので、是非ご視聴ください。

icon
yosatak
開発支援チームでpixivのコードを型解析しています.PHP,Elixir,POSIX Shell Scriptが好きです.気軽に出国できていた頃は海外旅行が趣味でした