実践PHPStan

公開日:

沖縄県那覇市ZORKS沖縄 および YouTube Liveで開催された『PHPカンファレンス沖縄2022』でレギュラートーク(30分)として発表しました。

Download PDF

スライドテキスト

Page 1

実践PHPStan

Practical PHPStan

pixiv Inc.
USAMI Kenta

PHP Conference Okinawa 2022

2022-08-27

Page 2

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE / にゃんだーすわん
  • ピクシブ株式会社 pixiv事業本部 エンジニア
    • 最近はピクシブ百科事典(dic.pixiv.net)を開発しています
  • Emacs Lisper, PHPer
    • Emacs PHP Modeを開発しています (2017年-)

Page 3

tadsanのあれこれが読める場所

  • https://tadsan.fanbox.cc/
  • https://scrapbox.io/php/
  • https://www.phper.ninja/
  • https://zenn.dev/tadsan
  • https://qiita.com/tadsan
  • https://github.com/bag2php

Page 4

https://www.phper.ninja/

Page 5

https://scrapbox.io/php/

Page 6

今回のお題

Page 7

プロポーザル

Page 8

先日、型についての話もしました

Page 9

PHPカンファレンスでも話しました

Page 10

今回のゴール

PHPStanで型を付けて
既存コードの型を検査

Page 11

今回やらないこと

PHPStanの詳細な設定
CI環境の組み方
ジェネリクス/条件型

Page 12

ジェネリクスについて

Page 13

条件付き戻り値型

Page 14

このスライドは

本日中に公開されます

Page 15

この発表では知っておくべき機能 を雑に取り上げるので本番運用/ 提案の前にドキュメントを自身で
しっかり読み込みましょう。

Page 16

それでもわからないことがあれば TwitterかSlackのphpusers-ja
#type-safeで尋ねてください

Page 17

PHPStan
使ってますか?

Page 18

Page 19

PHPStanとは何か

  • 静的に(=プログラムを動作させずに)コードの状態を解析するツール
    • 広い意味ではコードのフォーマッターやタグ生成なども含む

リ ン タ ー

  • PHPStanは静的解析の中でもLinterと呼ばれるジャンルのツール
  • ⇔ 動的解析 (静的解析の対義語)
    • XdebugやPHPUnitはプログラムを動かしてみて性質を確かめる
    • 従来のPHPStanは静的と動的のハイブリッドだった

Page 20

なに? 使ってない?

Page 21

あなたとPHPStan いますぐダウンロー

Page 22

どうやって?

Page 23

Ond ej said...
ř

Page 24

PHP 7.2以上ならプロジェクトに追加しよう

composer require--dev

phpstan/phpstan

Page 25

プロジェクトに追加できないなら

composer globalrequire

phpstan/phpstan

Page 26

require --dev以外の方法を検討

  • 要は何らかの事情があってcomposer.jsonに追加できないなら仕方ない
    • プロジェクトがPHP 7.2以前に依存しているとき
    • 権限の問題などでcomposer.jsonに追加でないとき
  • それ以外は原則Composerに追加しましょう
    • PhpStormの最新版なら、いい感じに勝手にPHPStanを動かしてくれる
    • もっと詳細な話はぺちこん沖縄当日までの間にzennに書きます

Page 27

PHPStanを入れてどうするの

  • Composerの実行ファイルは ./vendor/bin/phpstan にある
    • 以下、パスを省略して単に phpstan コマンドとして表記
  • 基本は phpstan analyze のように動かすと解析できる
    • phpstan analyze src/Foo/Target.php でファイル単位解析
  • PHPStan ProというWebインターフェイスもある
    • 弊社では応援の意味も込めてProを契約しています

Page 28

PhpStormがあるのに必要…?

  • PhpStormもがんばってPHPStanやPsalmとの互換性を高めているが、
    解析能力に関しては専門のツールの方が強い
    • PHPStanとPsalmはユーザーがPHPで自由に拡張できる
  • PhpStormの方がレガシー寄りのPHPバージョンに強い
    • PHPStanはPHP 7.2を以上サポート
    • PhpStormのヘッドレス版のQodanaというツールがCI用途に使える

Page 29

解析結果はリアルタイム表示しよう

  • PhpStorm: 最近のバージョンでは標準組み込み

(場合によって要設定)

  • VS Code: sordev.phpstan ErrorLens

+

(Marketplaceには古いのもあるので注意)

  • Vim/NeoVim: coc.nvimとかALEを入れるといいと思います
  • Emacs: Hycheck-phpstan

(私が作っています)

  • その他のLSP対応エディタ: efm-langserver

(汎用言語サーバー)

Page 30

外した方がいいPhpStorm設定

Page 31

外した方がいいPhpStorm設定

arrayと
array<mixed>と
mixed[] を混同しない

Page 32

外した方がいいPhpStorm設定

@param とか @returnを
執拗に全部埋めてくる

arrayと
array<mixed>と
mixed[] を混同しない

Page 33

ここ最近のPHPStan

  • 完全静的解析モードがデフォルトになりました
    • いままでのPHPStanは実行時リフレクションで解析コストを抑えていた
    • 改善の積み重ねによりパフォーマンスがものすごく向上している
    • 従来通りの実行時リフレクションも使える
  • PHPStanはハイペースで改善されているのでガンガン更新しましょう
    • ただし今月は開発者が夏休みを宣言している

Page 34

PHPStanは何ではないか

  • PHPのあらゆる関数に対して完璧な型を付けてくれるわけではない
    • 不具合や意図しない挙動を発見したら報告してほしい
    • Twitterに書くだけでも作者か日本人の誰かが反応してくれると思う
  • オープンソースソフトウェアであり、商用サポートはない
    • チェコ在住の個人 が開発している

(Ondřej Mirtes)

  • コミュニティによるバグ修正や機能追加の貢献も受け入れている

Page 35

PHPの型の基礎

Page 36

型の基本的な考え方

  • 基本は型宣言できるPHPの型を覚える
    • クラス/インターフェイス名 (PHP 5)
    • 複合型 array / callable (PHP 5)
    • スカラー値 int / Hoat / string / bool (≧ PHP 7.0)
    • 特別な戻り値型 void (≧ PHP 7.0) / never (≧ PHP 8.1)
    • そのほかの型表記

Page 37

ユニオン型

A|B

AまたはB
(複雑な式は書けない)

Page 38

交(cid:20281)型

A&B

AおよびB
(複雑な式は書けない)

Page 39

DNF
int|A|(B&C)|null

intまたはAまたは「BおよびC」またはnull
(一定の制約下で複雑な型も書ける)

Page 40

null許容型

?
A

Aまたはnull
デフォルトで許容しない

Page 41

謎のPHP用語

mixed

任意の(すべての)型を表す型
(ほかの言語でのany)

Page 42

型をどこに書くか

Page 43

型宣言

(type declaration)

Page 44

型宣言

(type declaration)

Page 45

型宣言

(type declaration)

Page 46

型宣言

(type declaration)

Page 47

型宣言

(type declaration)

Page 48

型注釈

(type annotation)

Page 49

型注釈

(type annotation)

Page 50

型注釈

(type annotation)

Page 51

型宣言 vs PHPDoc(型注釈)

  • 型宣言された型は、実行時に100%確実に保証される実効性のある型です
  • PHPDocは /** ... */ 形式のブロック内に書かれる単なるコメントです
    • 実行時にはその通りの型であることは保証されず、型は口約束に過ぎません
  • この発表内で「型を付ける」というときは型宣言と型注釈のどちらも使います
    • 実効性のある型と口約束に過ぎない型の二枚舌の使い分けが
      動的言語における現実的な型の付け方になります

Page 52

無駄なPHPDocは書かない

  • 型宣言と同じ内容をPHPDocに書き写さない
    • 自然言語で説明を書きたいときは書く
    • int→positive-int string→定数 のように型を強化する場合は書く
  • 継承元と同じ型の場合は何も書かない (共変型付けをしたいときは書く)
    • PHP: 共変性と反変性 - Manual
  • 几帳面に埋めようとしてくるPhpStormの言うことを聞かない

Page 53

外した方がいいPhpStorm設定

(再掲)

Page 54

外した方がいいPhpStorm設定

(再掲)

arrayと
array<mixed>と
mixed[] を混同しない

Page 55

外した方がいいPhpStorm設定

(再掲)

@param とか @returnを
執拗に全部埋めてくる

arrayと
array<mixed>と
mixed[] を混同しない

Page 56

型宣言があれば
PHPDocなくても
足りるんじゃないの?

Page 57

PHPStanには
もっと強力な型がある

Page 58

PHPStanを 使ってみよう

Page 59

オンライン
サンドボックス

Page 60

いますぐダウンロー

Page 61

いますぐダウンロー

しなくてもいい

Page 62

https://phpstan.org/try

Page 63

こういう関数を
考えてみましょう

Page 64

型宣言で型のついた完璧な関数

Page 65

型宣言で型のついた完璧な関数

本を検索する関数

Page 66

型宣言で型のついた完璧な関数

本を検索する関数

検索ワード

Page 67

型宣言で型のついた完璧な関数

本を検索する関数

検索ワード

何これ

Page 68

型宣言で型のついた完璧な関数

本を検索する関数

検索ワード

何これ

何これ

Page 69

PHPStan第一の関門(レベル6)

Page 70

PHPStan第一の関門(レベル6)

$options
配列の中身わからん

Page 71

PHPStan第一の関門(レベル6)

$options
配列の中身わからん

returnされる
配列の中身わからん

Page 72

再度完璧に型を付けた関数

Page 73

再度完璧に型を付けた関数

配列の中身には あらゆる可能性

Page 74

型を表示してみる

Page 75

取り出してきた値の配列

Page 76

取り出してきた値の配列

配列の中身わからん

Page 77

型がついていないとはどういうことか

  • プログラムで具体的なことをするために、必要な情報が揃っていない
    • 今回の場合だと「検索して取得した本の情報を画面に出力する」など
    • 配列の中にどのような値が入っているのか内部構造が明示されてない
  • 多くの場合、mixed型の場合は具体的な操作をするための情報が欠けている
  • PHPStanは値を引数として渡すときに、要求する型に対して 渡す型が十分に絞り込まれていないと容赦なく叱ってくれる

Page 78

このプログラムが正しいか判断できない

Page 79

必要のないところにmixedは書かない

  • アプリケーション実装においてmixedと適切な場面はあまりない
    • mixed = いかなる種類のデータも受け入れるということ
  • ビジネスロジックというよりユーティリティ的なものが該当する
    • mixedが適切なのはvar_dump(), is_string() のような関数
    • array<mixed> を受け入れるのはcount()とかsort()とかだけ
  • ジェネリクスを使えばmixedと型付けを両立できるが、今回は割愛

Page 80

コードに型を付ける

Page 81

型宣言があれば
足りるんじゃないの?

Page 82

型宣言だけでは
表現力が不足している

Page 83

array
“配列”の曖昧さ

Page 84

別途arrayの話もしました

Page 85

配列はひとつ!じゃない!!

  • ユーザIDがならんだリスト [123, 456]
  • ユーザIDと名前の対応表 [123 => '野比のび太', 234 => '源静香']
  • ユーザを表す構造体 ['id' => 123, 'name' => '野比のび太']
  • ユーザを表す構造体がならんだリスト
    [
    ['id' => 123, 'name' => '野比のび太'],
    ['id' => 234, 'name' => '源静香'],
    ]

Page 86

配列はひとつ!じゃない!!

list<int>

  • ユーザIDがならんだリスト [123, 456]
  • ユーザIDと名前の対応表 [123 => '野比のび太', 234 => '源静香']
  • ユーザを表す構造体 ['id' => 123, 'name' => '野比のび太']
  • ユーザを表す構造体がならんだリスト
    [
    ['id' => 123, 'name' => '野比のび太'],
    ['id' => 234, 'name' => '源静香'],
    ]

Page 87

配列はひとつ!じゃない!!

list<int>

array<int,string>

  • ユーザIDがならんだリスト [123, 456]
  • ユーザIDと名前の対応表 [123 => '野比のび太', 234 => '源静香']
  • ユーザを表す構造体 ['id' => 123, 'name' => '野比のび太']
  • ユーザを表す構造体がならんだリスト
    [
    ['id' => 123, 'name' => '野比のび太'],
    ['id' => 234, 'name' => '源静香'],
    ]

Page 88

配列はひとつ!じゃない!!

list<int>

array<int,string>

  • ユーザIDがならんだリスト [123, 456]
  • ユーザIDと名前の対応表 [123 => '野比のび太', 234 => '源静香']
  • ユーザを表す構造体 ['id' => 123, 'name' => '野比のび太']
  • ユーザを表す構造体がならんだリスト

array{id:int, name:string}

[
['id' => 123, 'name' => '野比のび太'],
['id' => 234, 'name' => '源静香'],

]

Page 89

配列はひとつ!じゃない!!

list<int>

array<int,string>

  • ユーザIDがならんだリスト [123, 456]
  • ユーザIDと名前の対応表 [123 => '野比のび太', 234 => '源静香']
  • ユーザを表す構造体 ['id' => 123, 'name' => '野比のび太']
  • ユーザを表す構造体がならんだリスト

array{id:int, name:string}

[
['id' => 123, 'name' => '野比のび太'],
['id' => 234, 'name' => '源静香'],

]

list<array{id:int, name:string}>

Page 90

array-shapes
(またはobject-like arrays)

Page 91

arrayに詳細に型を付ける

Page 92

arrayに詳細に型を付ける

fullかpartialのみ

Page 93

取り出してきた値の配列

Page 94

取り出してきた値の配列

Page 95

取り出してきた値の配列

fullかpartialなのに

perfect

Page 96

取り出してきた値の配列

fullかpartialなのに

perfect

ほんとはtitleなのに
nameでアクセス

Page 97

PHPStanの
強力な型

Page 98

型名について

  • 型名は一般的に array, int, string, bool などすべて小文字で書く
  • クラス名は慣習的に DateTime, App Book のようなキャメルケース

(先

\

が一般的だが、bookのように小文字も定義可

頭と単語区切りを大文字にする)

  • そのためツールによっては @return never と書くと App と解釈

\never

  • これを避けるには @param @return @var に代えて、
    @phpstan-param @phpstan-return @phpstan-var にする

Page 99

整数範囲型

  • int<0, 6> や int<1, 10>のような範囲を指定できる
  • positive-int, negative-int という型もある
    • それぞれ int<1, max> / int<min, -1> のエイリアス
  • 「0以上の数」はユニオン型を組み合せて 0|positive-int と書く
    • たとえば「配列の要素数」にマイナスはありえないのでこの型になる
  • int === int<min, max> === positive-int|0|negative-int

Page 100

リテラル型・定数型

  • "full" や "partial" のような文字列や 0, 1 のようなスカラー値そのもの
  • ふつうはユニオン型と組み合せて "full"|"partial" や 1|2|9 などと書く
  • constやde=neで定義された定数・クラス定数も使える
    • SEARCH_MODE_FULL|SEARCH_MODE_PARTIAL
    • SEARCH_MODE_* でまとめて参照もできる
      • 新たな SEARCH_MODE_FUZZY が増えても変更の必要がない

Page 101

key-of型・value-of型

  • 定数に格納された配列のキーまたは要素
  • const MODES = ['a', 'b']; のとき value-of<MODES> = 'a'|'b'
  • const MESSAGES = ['x' => 'あ', 'y' => 'ん'] のとき
    • key-of<MESSAGES> = 'x'|'y'
    • value-of<MESSAGES> = 'あ'|'ん'
  • 定数の * と同じく、定数の配列を編集すれば個別の箇所を修正しなくていい

Page 102

PHPDocに書ける配列の型について

  • 最近は string[] や Book[] のような書かれかたは好まれない
    • JavaScript(TypeScript)のように配列/リストと連想配列
      (Map/ハッシュテーブル)の区別がない
    • list<string>やarray<string,Book>のように書くのが望ましい
  • 配列の中身に同じ構造のものが繰り返されるときは array<...>
    配列の中身がキーごとに別のデータが格納されるときは array{...}
    • この <> と {} のカッコの種類は瞬時に混乱しないようにしておきましょう

Page 103

array-shapes

(Object-like arrays)

  • array{key: type} keyというキーが入っている連想配列
  • array{key?: type} keyというキーが省略されている連想配列
  • array{0: typeA, 1: typeB} 長さ2の配列(リスト)
    • array{typeA, typeB} この形状ではキーを省略して型だけを列挙できる
  • array{} 中に何も入っていない配列(空配列)

Page 104

array<Type>

  • array<Type> 配列の全ての値はType (キーの型が何かは明示しない)
  • array<string, Type> キーの型がstring, 値の型がType
  • array<array{key: value}>
    • 配列の中にarray-shapeが入っている

Page 105

list<Type>

  • ['x', 'y', 'z'] のようにキーが0, 1, 2, ... のような連番の配列
    • PHP 8.1で追加された array_is_list() 関数が根拠
  • list<Type> リストの全ての値はType
    • 現在のPHPStanは array<int, Type> のエイリアス扱い
  • list<array{key: value}>
    • 配列の中にarray-shapeが入っている

Page 106

non-empty-*型

  • non-empty-string: 長さ1以上の文字列 (=空文字列ではない)
  • non-empty-array: 長さ1以上の配列 (キーの型は問わない)
  • non-empty-list: 長さ1以上のリスト

Page 107

先程のコードを拡張する

Page 108

先程のコードを拡張する

検索モードのリスト

Page 109

先程のコードを拡張する

検索モードのリスト

value-of<定数>

Page 110

先程のコードを拡張する

検索モードのリスト

value-of<定数>

価格は正の整数

Page 111

先程のコードを拡張する

検索モードのリスト

value-of<定数>

空文字列を
検索禁止する

価格は正の整数

Page 112

呼出し側のコード

Page 113

呼出し側のコード

(警告)
空文字列を検索

Page 114

呼出し側のコード

(警告)
空文字列を検索

(警告)
price == 0 はありえない

Page 115

ばっちり型が つきましたね

Page 116

基本的な型の付け方

  • 関数/メソッドとクラス定義に絞って詳細な型を付けていく
    • パラメータ (仮引数)
    • 戻り値
    • プロパティ
  • ただしクロージャ(無名関数)はがんばらなくていい
  • 基本はこれだけに絞っていけばコードに型は付く (理想論)

Page 117

array{} とか
覚えなくてよくない?

Page 118

そういう意見もある

Page 119

DTO

(Data Transfer Object)

  • データを格納して持ち運ぶことを目的としたクラス
  • C言語などの構造体(struct)をクラスで表現したものとも考えられる
  • 典型的には単なるPOPOで実装できる
  • PHP 8.0からはコンストラクタプロモーションで、異様に簡潔に定義できる
    • 現代PHPでは小さいイミュータブルなクラスをバンバン切っていく方が
      責務が小さく解析もしやすくなるが今回の本題ではないので割愛

Page 120

PHP 8.1時代のコード

Page 121

PHP 8.1時代のコード

検索モード

Page 122

PHP 8.1時代のコード

検索モード

オプションは配列
ではなくクラス

Page 123

PHP 8.1時代のコード

Page 124

PHP 8.1時代のコード

著者

Page 125

PHP 8.1時代のコード

著者

書籍

Page 126

PHP 8.1時代のコード

著者

書籍

コンストラクタ プロモーション

Page 127

PHP 8.1時代のコード

Page 128

PHP 8.1時代のコード

オプション型宣言

Page 129

PHP 8.1時代のコード

オプション型宣言

戻り値PHPDoc

Page 130

クラス vs array

  • 一般論としては何でも格納できる配列より、クラス定義したオブジェクトの方
    が静的解析もしやすく、実行時に意図しない値も紛れにくい
  • 特にPHP8ではコンストラクタプロモーションによって定義のハードルが 一気に下がり、名前付き引数によってコード上に意図も込めやすくなった
  • クラスは名前を付けて定義するので、似て非なる別のクラスを
    いくつも作ることになるとしんどくなってくる
    • 単なるデータとして名無しのarray-shapesが良いこともある

Page 131

かくしてPHPに
完璧な型がつきました

Page 132

めでたしめでたし

Page 133

😇

Page 134

これで済むなら
PHPは型なしとか
呼ばれてない

Page 135

型なしは
どこから来るの?

Page 136

動的型 vs 型なし

  • 型宣言しなくても、全ての値は実行時に必ず具体的な型情報を持っている
    • get_debug_type() や is_string() などで動的に正しく判別できる
    • 型宣言すると
  • ここで言いたい「型なし」はソースコードを見て具体的な型が定まらないもの
    • 実行してみれば「動く」とわかるが、実行するまで正しいとは判断しきれない

Page 137

型なしの値

  • PHPではいろんなところから型がついてない値が吹き出してくる
    • そのまま使えない mixed, array<mixed>, string, string|array, ...
  • スーパーグローバル変数($_GET)、PDO、$args、fgetcsv()、などなど…
  • そのまま使えると信じ込んで(=間違った値は来ないと祈って)使う

😇

  • → 一種の割り切りではあるが、最後の手段にしたい

Page 138

型を絞り込もう

Page 139

PHPStan dumpType()
\

Page 140

dumpType()関数で型を表示

Page 141

dumpType()関数で型を表示

関数に渡す

Page 142

dumpType()関数で型を表示

関数に渡す

Page 143

dumpType()関数で型を表示

関数に渡す

もちろんPhpStorm上
で表示できる

Page 144

check & throw

Page 145

check & throw

falseだったら
関数を抜けることで

Page 146

check & throw

falseが混じる

falseだったら

可能性を排除

関数を抜けることで

Page 147

assert

(表明)

Page 148

assert

(表明)

falseではないと表明

Page 149

assert

(表明)

falseが混じる 可能性を排除

falseではないと表明

Page 150

PHPStanは分岐ごとに型を保持

Page 151

PHPStanは分岐ごとに型を保持

分岐ごとに範囲が分かれ
合流すると戻る

Page 152

DIコンテナ

Page 153

任意の値を格納できるコンテナ

Page 154

超適当に実装して

Page 155

値をコンテナに詰め込む

Page 156

値をコンテナから取り出す

Page 157

値をコンテナから取り出す

Page 158

値をコンテナから取り出す

Page 159

値をコンテナから取り出す

変数すべてmixed

Page 160

値をコンテナから取り出す

変数すべてmixed

型が不定のメソッド
呼び出し

Page 161

assertによるインライン型付け

Page 162

@varによるインライン型付け

Page 163

if/assert vs @var

  • if文/assertなどは単純な型宣言よりも詳細に型付けできる
    • 本番でassert式の内容を評価するかは設定で制御できる(マニュアル参照)
    • 私は無効化しない方が安全だと思います
  • @varはarray-shapesなどを使って配列の構造も詳細に型を付けられる。 ただし単なるコメントなので実態に反する型も付けられてしまうので要注意

Page 164

if/assert vs @var の使い分け案

  • 変数を生成/取り出した直後に引数としてそのまま渡す場合は、
    実装側の責任とする/型宣言による自動型チェックで保証されるので @var
  • DBのクエリ結果などは@varで済ませるか、hydratorライブラリを使って
    期待通りの構造の配列/クラスにマッピングする
  • 変数を生成/取り出した直後にそのオブジェクトのメソッド呼び出しをする
    場合は、 assert($var instaceof Foo) のようにチェックしておくことで
    エラーメッセージがわかりやすくなる

Page 165

標準関数 vs 型

  • 失敗したらfalseを返す標準関数が多い
  • =le_get_contents(): string|false
    • 存在しないファイルにアクセスするとWarningを発生
    • Warningなどのエラーはset_error_handler()で振る舞いが変わる
  • $content = =le_get_contents(); assert($content !== false);
  • Safe PHP: 全ての標準関数の失敗を例外に変換してくれるライブラリ

Page 166

簡潔だが型安全ではないコード

Page 167

几帳面にチェックする実装

Page 168

実行時/型安全なラッパー関数

Page 169

まとめ

Page 170

PHPStanは オンラインで
型チェックできる

Page 171

https://phpstan.org/try

Page 172

PHPStanは 完璧じゃない

Page 173

期待した型がつかない
と思ったら最小の
コードをオンラインで
チェックしましょう

Page 174

バグの場合もあるし 実装してPRを送る
チャンスかもしれない

Page 175

今回スルーした型

Page 176

ジェネリクス

Page 177

条件付き戻り値型

Page 178

そもそも自明な型は付けたくない

Page 179

こういう型を付けられると嬉しい

Page 180

現在のPHPStanの
仕様上このような型は
つけられない

Page 181

Page 182

そのうち

(誰かが実装完了すれば) 改善する見込みはある

Page 183

PHPStanの実装は
超簡単だとは言えないが 仕事でPHPやってたら 手を出せないほど難解と
いうほどではない

Page 184

みなさんもチャレンジ
してみましょう

Page 185

PHPStanで
楽しい型付けライフを