Skip to content

PHP型検査・夢と理想と現実

公開日:

福岡市博多区福岡ファッションビル・FFBホールで開催された『PHPカンファレンス福岡2019』でレギュラーセッション(15分)として発表しました。

Download PDF

スライドテキスト

Page 1

PHP

型検査・夢と理想と現実

PHP typing: the gap between ideal and reality.

補訂公開版

[2019-06-29]PHPCon Fukuoka 2019

FFBHall #phpconfuk

Page 2

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE
  • GitHub/Packagistでは id: zonuexe
  • ピクシブ株式会社 pixiv事業本部
  • Emacs Lisper, PHPer
  • Emacs PHP Modeのメンテナ引き継ぎました
  • 好きなリスプはEmacs Lispです
  • Qiitaに記事を書いたり変なコメントしてるよ

Page 3

Page 4

おしながき

Page 5

おしながき

【はやわかり】 PHPと型

型付けと型解析の考えかた

型と実装上の問題

Page 6

最初から結論

Page 7

PHPの型付けは難しい

Page 8

PHPのスカラー型宣言は難しい

Page 9

PHP7だからと言って、やみくもにstringやintなどの型宣言を書くのはおすすめできない

Page 10

思考停止でのstrict_types=1指定もおすすめできない

Page 11

コーディングにはPhpStormやPHPStanを活用しましょう

Page 12

静的解析でCIしましょう

Page 13

型宣言だけではなくPHPDoc/PSR-5の型も活用しよう

Page 14

型の記法だけでは解決できないことがあるので実装を工夫しよう

Page 15

そんな話をします

Page 16

【はやわかり】PHPと型

Page 17

PHPと型

いわゆる「動的型」の言語

PHP処理系は変数に型がない

変数にはどんな値も代入可能

PHP7の型には二種類のモード(strict_types)がある

Page 18

PHPの型の種類

スカラー型

複合型

特殊型

ドキュメント上の擬似型

Page 19

スカラー型

論理型 (bool) true, false

整数型 (int) 1, 2, 3...

浮動小数点数 (float) 1.0,...

文字列型 (string) "abc"

Page 20

複合型

配列型 (array)

オブジェクト型 (object)

呼び出し可能型 (callable)

反復型 (iterable)

Page 21

特殊型

ヌル型 (null)

リソース型 (resource)

型ではないが返り値に書ける

void 返り値がないこと

Page 22

ドキュメント上の擬似型

mixed 多様な型(複数)

number int|false

その他ツール固有の型表記

Page 23

declare(strict_types)

ブロック(ファイル)単位でディレクティブを設定するための構文

デフォルト(無指定時)はstrict_types=0

引数と返り値についてキャストとエラーの振舞に影響(後述)

Page 24

declare(strict_types=0)

引数や返り値のスカラー型宣言と実際の値が一致しなかったとき、変換可能な値なら暗黙的に型変換

オブジェクトが暗黙的にキャストされることはない

Page 25

declare(strict_types=1)

引数や返り値の宣言と実際の値が一致しなかったときTypeErrorが発生

スカラー型・その他の型・クラス/インターフェイスで挙動に違いはない

Page 26

PHPのどこに型を書けるか

引数の型宣言

返り値の型宣言

PHPDoc

Page 27

引数の型宣言

関数/メソッドの仮引数リスト

function x(string $s, array $a)

のように書ける

書けるのは一部の型・擬似型とクラス・インターフェイス

Page 28

返り値の型宣言

仮引数リストの後に書ける

function y():intのように書ける

基本は引数と同様

値を返さない場合に : void と書ける

Page 29

型宣言のわかりにくい仕様

書ける型と書けない型がある

self, array, callable, bool,float, int, string,iterable(7.1+), object(7.2+)だけが特別な意味を持つ

それ以外はクラス/インターフェイス

Page 30

型宣言に書けない型

integer, boolean, doubleなどの型の別名は記述不可

ちなみにstrはstringの別名…ではないので覚えておいてください (型宣言に限らず)

resourceは型宣言に書けない

mixedやnumberも書けない

Page 31

PHPDocの型注釈記法

クラスやメソッドの上にDoc Commentを書くことで型を記述できる

あくまでコメントなのでコードの実行に影響を及ぼさず、IDEやツールの解析材料となる

Page 32

PHPDocの型注釈記法

型宣言では実現できない型が付けられる

マジックメソッド/プロパティ

ユニオン型 Hoge|Fuga

配列要素の型付け int[]

Page 33

PSR-5 (Draft)

array<string>やArrayObject<int,string>と書ける

……が、PhpStormは対応してない

ArrayObject|string[]で代用

Page 34

ここまでは基本的な話

Page 35

型付けと型解析の考えかた

Page 36

関数がどのような引数を期待しどんな値を返すのか明示したい

Page 37

オブジェクトのメソッドやプロパティが適切に補完されると気持ち良い

Page 38

プログラムが間違ってることを実行前に検証したい

Page 39

ツールを導入しよう

Page 40

現行の高度な静的解析ツール

PhpStorm (IDE)

Phan

PHPStan

Psalm

Page 41

Page 42

Page 43

Page 44

PhpStorm

よいところ

導入が最も簡単

十分な精度のエラー検出

よくないところ

エラーの列挙ができないのでCIのフローに組み込めない

Page 45

Phan

よいところ

導入は比較的簡単

とても高い精度のエラー検出

よくないところ

大規模プロジェクトでは解析に時間がかかる

Page 46

Phanのすごいところ

連想配列にも型が付けられる

array-shapes記法

自動で型推論もしてくれるが解析がすごすぎてレガシーで長大な関数から恐ろしい型が吐かれたりする

Page 47

PHPStan

よいところ

実行時情報を使用し高速

高い精度のエラー検出

よくないところ

Composer以外の依存関係があると導入にコツが要る

Page 48

Psalm

よいところ

独自の引数アサーション

高い精度のエラー検出

よくないところ

大規模プロジェクトでは解析に時間がかかる

Page 49

つまりどれがいいのか (CI)

現時点ではそれぞれのツールで検出項目に一長一短があるため、使い分けるとよい

PHPStanは高速なのでGitでブランチにpushするごとに検査するようなCIと相性がよい

Page 50

(エディタ)

つまりどれがいいのか

PhpStormは総合的に見て簡単に導入できて強力

自分で自由にハックしたいとか、どうしても予算が出ないなどの事情があればエディタとPHPStanがおすすめ

Page 51

Page 52

プログラムが間違ってることを実行前に検証したい

Page 53

“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

Page 54

型解析の基本的な考え

Page 55

これから説明する解析プロセスは便宜的に単純化したもので、実際のツールが報告する警告とは異なります

Page 56

単純なコードで考える

strlen(string $s): int

$length = strlen($a);

$aには string が代入

$lengthは int が

されてるはず

代入される

Page 57

単純なコードで考える

$aは array が代入される

なんでstrlenに array を渡してるの?

$lengthは int が

strlen(string $s): int

代入される

なんでintと array を足してるの?

$a = explode('/', 'a/b');

$length = strlen($a);

$b = $length + [];

このコードに矛盾があるのは「実行しなくても」わかる

Page 58

関数の型付けを考える

$aと$bって何を指定すればいいの?

<?php

?

?

?

string

string

string

結果の型は何?

function x($a, $b) {

return $a . $b;

}

文字列結合なので両辺の値はstring

Page 59

単純に解決できない関数

$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

のどちらか

Page 60

PHPの型推論の限界

原理上コードの解析だけでは完全な静的検査は不可能

変数やプロパティの動的性

関数や演算子の多態性

インピーダンスミスマッチ

Page 61

PHPの現実と向き合う

Page 62

PHPDocで地道に型をつける

歴史あるコードは引数の仕様が形式化されたPHPDocで書かれてなかったり、崩れた形で書かれてたりする

CIやIDEに磨かれてないと高確率で誤りを含んでる

Page 63

PHPDocに則ってない

//===========================// @params str $a// @return true または false//===========================function x($a, $b) {

Page 64

PHPDocに則るように修正

/*** @param string $a* @return bool*/function x($a, $b) {

Page 65

多態な関数設計を避ける

引数によって返り値の構造(型)が変化するような関数は避ける

例: parse_url()

引数一つなら連想配列を返す

第二引数に定数を渡すと文字列を返す

Page 66

mixed を避ける

引数や返り値が実装を見ないとわからなくなる

json_encodeやvar_dumpみたいに多様なものを受け付ける関数以外はmixedと書くべき箇所は少ない

Page 67

arrayも極力控える

単にarrayとだけ書くと、配列の中身はmixed状態

「文字列が並ぶ配列」はstring[]

「決まったキーを持つ配列」の記法は標準化されてない

Page 68

@phan-param, @phan-return

Phanのarray-shapes記法ではarray{name:string,age:int}のように記述できる

Page 69

数値とインピーダンスミスマッチ

何かにつけて鬼門

クエリパラメータやルーターの動的パラメータから数字列を取得する処理

PDOから取得してきた数値

Page 70

クエリパラメータ

// 正の整数だけを期待if (!is_numeric($_GET['user_id'])) {throw new BadRequest;}

$user_id = (int)$_GET['user_id'];

Page 71

URL動的パラメータ

/user/000001 とか /user/0000000001とか受け付けちゃって本当に大丈夫…?

Page 72

filter_var/filter_inputで安心?

filter_var(' 123', FILTER_VALIDATE_INT)

// int(123)

Page 73

PHPの整数の範囲

ビット数はアーキテクチャ依存の符号付き整数値

外部入力やDBのUNSIGNED BIGINTなカラムの値を(int)キャストして化ける

Page 74

スカラー型宣言とstrict_types

キャストとつきあっていく覚悟が必要

通常のキャストとも別のルールで暗黙変換されることを認識しないと想定しない変換が走ることがある

Page 75

strict_typesの範囲

declare(strict_types)はファイルごとに指定される

引数の型チェックは呼び出す側の指定が、返り値の型チェックは定義側の指定が反映される

Page 76

レガシーコードとスカラー型

引数リストにむやみにstringやintと書かない

PHPDocならキャストは起こらないので動作に影響はしない

Page 77

静的解析結果とどうやって向き合っていくか

Page 78

既存プロジェクトを検査すると

数千行にも及ぶおびただしい警告が出力されることもざら

しかしそれよりも、そのコードが「たしかに動いている」事実が重要

Page 79

大事なのは傷口を広げないこと

警告が出てるから間違ったコードではない

警告の量が減ればメンテしやすい状況に近付くのだと捉える

Page 80

差分を重要視する

リファクタリングの前後でPhanの出力ログのdiffをとり、行が増えなければ、間違った書き換えはしてない可能性は高い

ロジックの正しさは保障できないのでテストなどは別途必要