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

スカラー型

論理型 true, false

(bool)

整数型 1, 2, 3...

(int)

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

文字列型 "abc"

(string)

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 , object

(7.1+) (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をと
り、行が増えなければ、間違っ た書き換えはしてない可能性は
高い
ロジックの正しさは保障できな いのでテストなどは別途必要