Page 1
PHP
型検査・夢と理想と現実
PHP typing: the gap between ideal and reality.
補訂公開版
[2019-06-29]
PHPCon Fukuoka 2019
FFBHall #phpconfuk
公開日:
by USAMI Kenta @tadsan
に福岡市博多区の福岡ファッションビル・FFBホールで開催された『PHPカンファレンス福岡2019』でレギュラーセッション(15分)として発表しました。
PHP
型検査・夢と理想と現実
PHP typing: the gap between ideal and reality.
補訂公開版
[2019-06-29]
PHPCon Fukuoka 2019
FFBHall #phpconfuk
お前誰よ
うさみけんた (@tadsan)
/ Zonu.EXE
おしながき
おしながき
【はやわかり】 PHPと型
型付けと型解析の考えかた
型と実装上の問題
最初から結論
PHPの
型付けは難しい
PHPのスカラー 型宣言は難しい
PHP7だからと言って、 やみくもにstringやint
などの型宣言を書くのは
おすすめできない
思考停止での
strict_types=1
指定もおすすめできない
コーディングには
PhpStormやPHPStan
を活用しましょう
静的解析で
CIしましょう
型宣言だけではなく
PHPDoc/PSR-5 の型も活用しよう
型の記法だけでは解決 できないことがあるの
で実装を工夫しよう
そんな話を
します
【はやわかり】
PHPと型
PHPと型
いわゆる「動的型」の言語
PHP処理系は変数に型がない
変数にはどんな値も代入可能
PHP7の型には二種類の
モード(strict_types)がある
PHPの型の種類
スカラー型
複合型
特殊型
ドキュメント上の擬似型
スカラー型
論理型 true, false
(bool)
整数型 1, 2, 3...
(int)
浮動小数点数 1.0,...
(float)
文字列型 "abc"
(string)
複合型
配列型 (array)
オブジェクト型 (object)
呼び出し可能型 (callable)
反復型 (iterable)
特殊型
ヌル型 (null)
リソース型 (resource)
型ではないが返り値に書ける
void 返り値がないこと
ドキュメント上の擬似型
mixed 多様な型(複数)
number int|false
その他ツール固有の型表記
declare(strict_types)
ブロック(ファイル)単位でディレ クティブを設定するための構文
デフォルト(無指定時)は
strict_types=0
引数と返り値についてキャス トとエラーの振舞に影響(後述)
declare(strict_types=0)
引数や返り値のスカラー型宣 言と実際の値が一致しなかっ たとき、変換可能な値なら暗
黙的に型変換
オブジェクトが暗黙的にキャ
ストされることはない
declare(strict_types=1)
引数や返り値の宣言と実際の
値が一致しなかったとき
TypeErrorが発生
スカラー型・その他の型・
クラス/インターフェイスで
挙動に違いはない
PHPのどこに型を書けるか
引数の型宣言
返り値の型宣言
PHPDoc
引数の型宣言
関数/メソッドの仮引数リスト
function x(string $s, array $a)
のように書ける
書けるのは一部の型・擬似型 とクラス・インターフェイス
返り値の型宣言
仮引数リストの後に書ける
のように書ける
function y():int
基本は引数と同様
値を返さない場合に : void と
書ける
型宣言のわかりにくい仕様
書ける型と書けない型がある
self, array, callable, bool,
float, int, string,
iterable , object
(7.1+) (7.2+)
だけが特別な意味を持つ
それ以外はクラス/インターフェイス
型宣言に書けない型
integer, boolean, double
などの型の別名は記述不可
ちなみにstrはstringの別名… ではないので覚えておいてく
ださい (型宣言に限らず)
resourceは型宣言に書けない mixedやnumberも書けない
PHPDocの型注釈記法
クラスやメソッドの上にDoc Commentを書くことで型を
記述できる
あくまでコメントなのでコー ドの実行に影響を及ぼさず、 IDEやツールの解析材料となる
PHPDocの型注釈記法
型宣言では実現できない型が
付けられる
マジックメソッド/プロパティ
ユニオン型 Hoge|Fuga 配列要素の型付け int[]
PSR-5 (Draft)
array<string>や
ArrayObject<int,string>と
書ける
……が、PhpStormは対応し
てない
ArrayObject|string[]で代用
ここまでは
基本的な話
型付けと型解析の
考えかた
関数がどのような引数
を期待しどんな値を
返すのか明示したい
オブジェクトのメソッド
やプロパティが適切に
補完されると気持ち良い
プログラムが
間違ってることを
実行前に検証したい
ツールを
導入しよう
現行の高度な静的解析ツール
PhpStorm (IDE)
Phan
PHPStan
Psalm
PhpStorm
よいところ
導入が最も簡単
十分な精度のエラー検出
よくないところ
エラーの列挙ができないので CIのフローに組み込めない
Phan
よいところ
導入は比較的簡単
とても高い精度のエラー検出
よくないところ
大規模プロジェクトでは解析
に時間がかかる
Phanのすごいところ
連想配列にも型が付けられる
array-shapes記法
自動で型推論もしてくれるが 解析がすごすぎてレガシーで 長大な関数から恐ろしい型が
吐かれたりする
PHPStan
よいところ
実行時情報を使用し高速 高い精度のエラー検出
よくないところ
Composer以外の依存関係 があると導入にコツが要る
Psalm
よいところ
独自の引数アサーション 高い精度のエラー検出
よくないところ
大規模プロジェクトでは解析
に時間がかかる
つまりどれがいいのか (CI)
現時点ではそれぞれのツール で検出項目に一長一短がある
ため、使い分けるとよい
PHPStanは高速なのでGitで ブランチにpushするごとに検 査するようなCIと相性がよい
つまりどれがいいのか
(エディタ)
PhpStormは総合的に見て
簡単に導入できて強力
自分で自由にハックしたいと か、どうしても予算が出ない などの事情があればエディタ
とPHPStanがおすすめ
プログラムが
間違ってることを
実行前に検証したい
“Phan is a static analyzer for PHP that prefers to minimize
false-positives. Phan attempts to prove incorrectness rather than
correctness.”
‒ https://github.com/phan/phan
型解析の
基本的な考え
これから説明する解析プロ セスは便宜的に単純化した もので、実際のツールが報 告する警告とは異なります
単純なコードで考える
strlen(string $s): int
$length = strlen($a);
$aには string が代入
$lengthは int が
されてるはず
代入される
単純なコードで考える
$aは array が
代入される
なんで
strlenに array を
渡してるの?
$lengthは int が
strlen(string $s): int
なんで
代入される
intと array を
$a = explode('/', 'a/b');
足してるの?
$length = strlen($a);
$b = $length + [];
このコードに矛盾があるのは「実行しなくても」わかる
関数の型付けを考える
$aと$bって
何を指定すればいいの?
<?php
? ? ?
string string string
function x($a, $b) { return $a . $b;
}
結果の型は何?
文字列結合なので
両辺の値はstring
単純に解決できない関数
$aと$bって
(number $x, number $b): number
何を指定すればいいの?
<?php
? ? ?
(array $x, array $b): array
function x($a, $b) {
で、どっち?
return $a + $b;
}
結果の型は何?
PHPの + 演算子は
number + number
array + array
のどちらか
PHPの型推論の限界
原理上コードの解析だけでは
完全な静的検査は不可能
変数やプロパティの動的性
関数や演算子の多態性
インピーダンスミスマッチ
PHPの現実 と向き合う
PHPDocで地道に型をつける
歴史あるコードは引数の仕様 が形式化されたPHPDocで 書かれてなかったり、崩れた
形で書かれてたりする
CIやIDEに磨かれてないと
高確率で誤りを含んでる
PHPDocに則ってない
//===========================
// @params str $a
// @return true または false
//===========================
function x($a, $b) {
PHPDocに則るように修正
/**
* @param string $a
* @return bool
*/
function x($a, $b) {
多態な関数設計を避ける
引数によって返り値の構造(型) が変化するような関数は避ける
例: parse_url()
引数一つなら連想配列を返す 第二引数に定数を渡すと文字
列を返す
mixed を避ける
引数や返り値が実装を見ない
とわからなくなる
json_encodeやvar_dumpみ たいに多様なものを受け付け る関数以外はmixedと書くべ
き箇所は少ない
arrayも極力控える
単にarrayとだけ書くと、配列
の中身はmixed状態
「文字列が並ぶ配列」はstring[]
「決まったキーを持つ配列」の
記法は標準化されてない
@phan-param, @phan-return
Phanのarray-shapes記法で
は
array{name:string,age:int}
のように記述できる
数値とインピーダンスミスマッチ
何かにつけて鬼門
クエリパラメータやルーター の動的パラメータから数字列
を取得する処理
PDOから取得してきた数値
クエリパラメータ
// 正の整数だけを期待
if (!is_numeric($_GET['user_id'])) {
throw new BadRequest;
}
$user_id = (int)$_GET['user_id'];
URL動的パラメータ
/user/000001 とか /user/0000000001 とか受け付けちゃって本当に大丈夫…?
filter_var/filter_inputで安心?
filter_var(' 123', FILTER_VALIDATE_INT)
// int(123)
PHPの整数の範囲
ビット数はアーキテクチャ依
存の符号付き整数値
外部入力やDBの
UNSIGNED BIGINTなカラ
ムの値を(int)キャストして
化ける
スカラー型宣言とstrict_types
キャストとつきあっていく覚
悟が必要
通常のキャストとも別のルー ルで暗黙変換されることを認 識しないと想定しない変換が
走ることがある
strict_typesの範囲
declare(strict_types)は ファイルごとに指定される 引数の型チェックは呼び出す
側の指定が、返り値の型
チェックは定義側の指定が反
映される
レガシーコードとスカラー型
引数リストにむやみにstring
やintと書かない
PHPDocならキャストは起 こらないので動作に影響はし
ない
静的解析結果と
どうやって向き
合っていくか
既存プロジェクトを検査すると
数千行にも及ぶおびただしい 警告が出力されることもざら しかしそれよりも、そのコー ドが「たしかに動いている」
事実が重要
大事なのは傷口を広げないこと
警告が出てるから間違った
コードではない
警告の量が減ればメンテしや すい状況に近付くのだと捉え
る
差分を重要視する
リファクタリングの前後で Phanの出力ログのdiffをと
り、行が増えなければ、間違っ た書き換えはしてない可能性は
高い
ロジックの正しさは保障できな いのでテストなどは別途必要