コネヒト開発者ブログ

コネヒト開発者ブログ

面倒いの抜きでPHPの静的解析「Phan」を使ってみる with Docker

面倒いの抜きでPhanを使ってみる

あけましておめでとうございます!
サーバーサイドやってます金城(@o0h)です。

年末年始は毎年恒例の「ヘルシング全巻読み直し」をしていました。カッコいいですね・・

弊社ではPhanを用いた静的解析を行っております。
今回は、「多分、これが最も1番の最速さでPhanを試す方法になるんじゃないかな?」というお話をいたします。

cf) リファクタリング対象を選ぶ戦略を決めよう - コネヒト開発者ブログ

https://cdn.mamari.jp/authorized/5a522dc0-c5f0-4f0d-8f46-0016ac120004.jpg

tl;dr

  1. cloudflare/phan というイメージがあります
  2. これを使うと、「php7環境の用意」だとか「php-astの用意」とかいった手間を全部すっ飛ばしてPhanの静的解析が実行できます
  3. すぐ使えるようにコマンドがあるので、↑のページの "Getting docker-phan" のセクションを参照してください*1

おしながき

概要

これは「静的解析したいけど環境作るの面倒くさい(そもそもphp7じゃない!)」という時のお供(になると思います)。

「Phanの実行に必要なもの全部入り」になっているイメージを使って、使い捨て感覚で静的解析を実行できるようになりました。
Cloudflare社が公開してくれているDockerイメージがそれを可能たらしめてくれているわけですが、こちらは本家のwikiからも言及されているものとなります。*2
そういう点では十分に信頼できるのかな?と思っています。

ただし、既存の「実際にアプリケーションコードが動作するPHP環境」とは違う「クリーンな状態(に近い)のPHP環境」になるため、少しハマった点もありました。
この記事では、「利用方法」にあわせて「実際にCakePHPアプリケーションを解析するときに用意したもの・ハマった点」を取り上げたいと思います。

解決したい問題

before(現状)

  • 本番環境とローカル環境で、あまり設定等を変更したくない
    • でも本番でphp-ast使うわけでもないし・・・いらないものを入れている状況
  • ローカル環境でxdebugを利用していて、「いつものコンテナの中でPhanをそのまま叩くと爆重」
    • でもいちいち「別設定ファイルを用意して、Phanのときにはそっちを使う」の面倒くさい・・
  • php71にしたら早くなるんだっけ?でも残念、全てのPJがphp71に出来ているわけではありません!
    • でもわざわざPhanのためにphp71環境を用意するのは(ry

after(嬉しい)

  • ローカル環境/本番環境で「php-ast/phanがいらない」状態に持っていける
  • 「すべてPhanのために用意しました」環境をつくれる
    • あくまでやりたいのは静的解析なので、そもそも「アプリケーションが正常に動くか」というのは別問題なわけで
    • php71!
    • xdebugも当然いない!
    • Alpine Linux、軽量、嬉しい

利用方法

例えば、こんな二通りのやり方があるかな?という気がしています。

A. ターミナルから即実行できるようにしておく

Getting docker-phan のセクションに説明がある内容です。 .bashrcなどに、以下のように記述しておきましょう

phan() { docker run -v $PWD:/mnt/src --rm -u "$(id -u):$(id -g)" cloudflare/phan:latest $@; return $? }

そして、おもむろにターミナルから phan -p -o analyze.txt などと叩きます。 すると「カレントディレクトリを対象に(./.phan/config.php があれば、それを読みつつ)、 -p(--progress-bar) オプション付きで、 analyze.txt にアウトプットする」という形で実行されます。 すなわち、「コマンドにくっつけられたオプションは、そのまま透過的に渡される」という形です。

B. 簡単に利用できるようにして配布する

チームメンバーのローカル環境を問わず「すぐに叩けるよう、共通化しておきたい」といったときはdocker-compose.ymlなどを用いて、コードと一緒にgit管理してしまうのも良いかと思います。 docker-compose.yml に以下のように記述します

version: '3'
services:
  phan:
    image: cloudflare/phan
    volumes:
      - .:/mnt/src:cached
    entrypoint: []
    command: sh -c "cd /mnt/src && /opt/phan/phan -p --color"

そうすると、 docker-compose run --rm phan とすれば呼び出し可能です。 この場合はPhanに渡すオプションが固定になるので、必要に応じてphanコンテナに任意のコマンドを渡してrunする形で利用すると良いと思います。

なにが行われているの?(ざっくり)

イメージのビルドについて

  1. PHP/composerのインストール
  2. Phanのインストール
  3. php-astのインストール

ENTRYPOINTについて

cloudflare/phan は何をしてくれるんだい、という部分ですね。
先に触れた docker-compose.yml で「commandをどう書けばいいのかな?」については、こちらを参考にしておりました。

  1. /mnt/srcにcdして
  2. exec php7 /opt/phan/phan "$@"を実行している

というシンプルなものですね。。 そのため、先に紹介しているコマンドをみても

  • docker run
  • -v $PWD:/mnt/src : 現在位置を「解析対象」としてコンテナにマウントする
  • --rm : 実行完了後に破棄
  • -u "$(id -u):$(id -g)" : 権限周りで問題になりにくいようにしつつ
  • cloudflare/phan:latest : 最新版のイメージを利用して
  • $@ : 透過的にオプションを渡す

という、ごくごく単純な処理しか要らなくなるわけです。

利用法については、これで終わりです。

CakePHPを対象に実行してみる

Phanの設定ファイルを書いてみる

.phan/config.php に諸々の設定を投げ込んでおくと、読みに行ってくれます。*3

こんな感じで動くのではないでしょうか。

<?php

return [
     // ロード対象
    'directory_list' => [
        'src',
        'vendor',
        '.phan/stubs'
    ],
    // ロード対象のうち、解析対象から除外するもの
    'exclude_analysis_directory_list' => [
        'vendor',
        'src/Console',
        '.phan/stubs',
    ],
    // 以下は好み
    'allow_missing_properties' => true, // @propertyの省略を許容
    'null_casts_as_any_type' => true,  // nullのキャストを許容
    'backward_compatibility_checks' => false, // 後方互換性はチェックしない
    'quick_mode' => true, // 対象の関数の中の関数を再帰的に検査しない
    'minimum_severity' => 10, // SEVERITY_CRITICALが対象
];

Phan\Exception\IssueException について(ハマった点)

さて、以上の設定を行った上で試しに走らせてみたら、動きませんでした!😫 どうも例外がスローされてしまいます・・・

Phan\Exception\IssueException in /opt/phan/src/Phan/AST/ContextNode.php:1261

あまり詳細な情報が出力されていなかったので、直接その付近のコードをいじりつつ眺めてみたら、どうもCake本体のコードが引っかかっているようです。
「\PDO::PARAM_BOOLへの参照があるけど、そんなクラスないよ!」みたいな内容でした。 「PDOクラス未定義ってなんだろうな〜」と暫くハマっていたのですが、 もしや?とおもってphp7-pdoをインストールしてみました。

apk add --upgrade php7-pdo

すると挙動が変わります。
ここで、「そういえば普段使っているのはDockerfileでゴニョゴニョしていたっけ〜」と思い出したわけです。

stubについて

docker-phanイメージをベースにコンテナ作るか?という考えも頭をよぎったのですが、 あくまで静的解析ができればよい(コードを動かすわけではない) ということで、Phanには「スタブ」という仕組みがあります。
How To Use Stubs · phan/phan Wiki
要するに、「クラスやプロパティやメソッドが定義されていれば良い」というゴールを設定できるのです。
以下の手順で使います。

  1. stubを定義する
  2. そのstubを配置したディレクトリを、config.phpから directory_list に設定する
  3. 同時に exclude_analysis_directory_list にも設定する

というステップで利用できます。
stubは、単純に「PHPのクラスを定義してphpdocコメントをつける」と言うものになり、新たにDSLなどを覚える必要はありません。

<?php

class AppDomainException extends \Exception
{
    /** @var string */
    public $hoge;
}

みたいなものです。
が!PHPのスタンダードなものについては、JetBrains社が配布しているものをそのまま利用することができます(Apache2ライセンス)

github.com

これはPHPStormを利用している方にはおなじみの、「標準クラス上で⌘+bを押したら開くアレ」になります。
今回は、こちらからPDO.phpを始めとしたいくつかのスタブを喰わせることで、無事に完走することができました。

まとめ

静的解析は「くらだないミスをなくし」たり、IDEを利用している身においては「普段のコーディングのストレスを軽減する」という効果もあると感じています。
ただし、PHPだと「やり始めるまでの腰が重い」のも事実でしょう。

これに対して、「使い捨てDockerイメージ」みたいなものは非常にインパクトが大きいと思います!正に巨人の肩に乗る感じですね。
未体験の方も、これを気に「とりあえず1回やってみようかな」くらいの感覚で遊んでみてはいかがでしょうか〜*4


お役立ちリンク

Phan導入当初も含めて、とても助けを得たURLたちです

*1:この記事を書き始めてから見つけたのですが、まさに「コマンドを使う」感覚でPhanを丸ごと実行できる!!というユーザー体験だと思います。すごい。。 → コマンド実行環境としてのDocker // Speaker Deck

*2:Getting started / From a Docker Image

*3:別箇所に置きたい場合は実行時に -k /path/to/config.php として渡せる

*4:実際の導入においては、 https://github.com/phan/phan/wiki/Incrementally-Strengthening-Analysis がとても参考になります