Slide 1

Slide 1 text

PHPの最高機能、配列を捨てよう!! At: PHPerKaigi 2023 Track A DateTime: 2023/3/25 10:20 (40min) Speaker: uzulla

Slide 2

Slide 2 text

uzulla フリーのPHPer 近況現場: PR TIMES (meiheiさんとかの所) Linkage (soudai1025さんとか 77webさんとか ごへいもちさんとかの所) (他にも色々) 何でも屋 詳しくはググってください

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

とのこと (ネット上の声をあつめてみました)

Slide 7

Slide 7 text

DISCLAIMER このトークは「独自解釈」「独自主張」「現在の自論」です 「正しい」ことは、自社の先輩にきいてください あるいは、よい書籍などから学び、よりよい手法を学んでください とにかく自分で選び取ってください

Slide 8

Slide 8 text

PHPの配列、好きですか? 私は好きです!!!!

Slide 9

Slide 9 text

最高機能、「PHPの配列」のすばらしさ あらゆるデータ構造がつくれる 他の言語の言葉を借りれば Vector, Map, Hash, Array, List, Iterable などを備える 数値や文字列をキーとしたListあるいはMapだが、順序が維持されカーソルを持つ 値は一種に制限されておらず、変数とできるものは何でも入る Push、Pop、Insert、Delete、Map、Filter、Grep等の言語組み込み関数がある つまり、PHPの配列一つでなんでもできる 他の言語より「PHPが簡単」といわれるゆえんの一つ (独自解釈)

Slide 10

Slide 10 text

普通の配列 // Array $array[] = "a"; $array[] = "b"; $array[] = "c"; foreach($array as $i){ echo $i; } // => abc echo $array[1]; // => b 「ふつうとは???」「普通がわからん」

Slide 11

Slide 11 text

ハッシュ的な探索 // 初期化は不要 $array['SOME'] = true; $array['THING'] = false; $array['flag'] = true; // HERE // ... if($array[$key_name_to_find]){ // $key_name_to_find = flag echo 'flag is enabled!'; } // => flag is enabled!

Slide 12

Slide 12 text

Queue, Stack. $array_queue = []; // 空の配列を作る array_push($array_queue, 1); // スタック( 最後に追加) array_push($array_queue, 3); // スタック( 最後に追加) array_splice($array_queue, 1, 0, 2); // 途中にインサート array_unshift($array_queue, 0); // 先頭にインサート echo array_shift($array_queue); // 先頭取り出し echo array_pop($array_queue); // 末尾とりだし array_push($array_queue, 4); // スタック( 最後に追加) echo array_shift($array_queue); // 先頭とりだし // => 0,3,1 // (2,4 is still in the queue)

Slide 13

Slide 13 text

柔軟な多次元操作、JSON等不定系データからのデコード (以下、真面目に読む必要はないです) $data1 = ["A"=>["B"=>[1], "C"=>"thing"], "D"=>"data"]; $data2 = ["α"=>["β"=>[new DateTime()], "γ"=>new StdClass()], "π"=>NULL]; $data3 = array_merge($data1, $data2); // Array を混合 $data3[0][1][2][3] = 'woo'; // 突然、深い配列要素を追加 unset($data3['α']['γ']); // 突然要素を削除( したら、その子もちゃんと消えます) $json_string = json_encode($data3); // JSON にコンバート( 自動キャスト込み) echo $json_string; // => {"A":{"B":[1],"C":"thing"},"D":"data","\u03b1": // {"\u03b2":[{"date":"2023-02-19 08:40:51.828780", // "timezone_type":3,"timezone":"UTC"}]},"\u03c0":null, // "0":{"1":{"2":{"3":"woo"}}}} $result = json_decode($json_string, true);

Slide 14

Slide 14 text

var_dump($result); array(5) { ["A"]=> array(2) { ["B"]=> array(1) { [0]=> int(1) } ["C"]=> string(5) "thing" } ["D"]=> string(4) "data" ["α"]=> array(1) { ["β"]=> array(1) { [0]=> array(3) { ["date"]=> string(26) "2023-02-19 08:39:14.925685" ["timezone_type"]=> int(3) ["timezone"]=> string(3) "UTC" }}} ["π"]=> NULL [0]=> array(1) { [1]=> array(1) { [2]=> array(1) { [3]=> string(3) "woo" }}}}

Slide 15

Slide 15 text

ルーター $routes = [ // キーを正規表現、値でアクションのクロージャー '#\A/\z#u' => function($p){echo "top page";}, '#/add/([0-9]+)/([0-9]+)#u' => function($p){ echo "{$p[1]} + {$p[2]} = " . $p[1] + $p[2]; }, '#.*#' => function($p){echo "Notfound";} ]; foreach($routes as $route => $action){ if($result = preg_match($route, $_SERVER['REQUEST_URI'], $m)){ $action($m); break; } } /add/1/2 のとき、 1 + 2 = 3 と表示

Slide 16

Slide 16 text

「生きている」DB接続などを保持できるコンテナ function getContainer(){ static $container = null; if(is_null($container)){ // 初期化, 二度目はスキップ $container = ['db' => new PDO('sqlite::memory:', null, null)]; } return $container; } // 〜〜〜 $stmt = getContainer()['db']->prepare('select 1 as a'); $stmt->execute(); $stmt->fetchAll(PDO::FETCH_ASSOC);

Slide 17

Slide 17 text

「最強では?!」 PHPの配列でほとんどのことはできるのだ!! できないこと…? イミュータブルとかreadonlyとか キーで「文字列の数値」と「数値」を別とするとか list とか 「そんなの人間がきをつければいいな!」 そしてPHP自体がフルスタックフレームワーク(要出典)なので 勢いで何かをシュッとつくる事において強い

Slide 18

Slide 18 text

しかし、強すぎる配列は毒になる そんな最強フリーダム機能を、わけもわからず初手で使わされる皆さん 最初に出会うこいつ: $_GET

Slide 19

Slide 19 text

クイズ!これは何!? function displayUserType($post){ echo $post['type']; } displayUserType($_POST);

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

AI「正解なんてわかるわけがねえだろ!」 まず、 $post とあるが $_POST と関係があるとはかぎらない (この例だと、目の前にあるからわかるが…) 配列なのでそのキーがあるかわからない もちろん、どういう値が入っているかもわからない 配列の場合だってあるよね 「なにもかもわからない…」

Slide 22

Slide 22 text

とりあえず、 偶然動くコード が書けてしまう (偶然に動くコードとは、トライアンドエラーで作られた 本人もあまり理解していないコードをここでは指します) 偶然なので、壊れやすい、人も機械も正しく理解できなくなる 「要件満たせたし、動くからヨシ!!」 PHPが歯ブラシといわれるゆえん(独自解釈) その成功体験から、それしか(しら)ない、正しいと思ってしまう

Slide 23

Slide 23 text

「…これは呪われし装備の類いでは?」 「あれえ?」 「ダジャレを言っている場合ではない」

Slide 24

Slide 24 text

突然、書き換えてみる enum UserType: string{ case UNCONFIRMED = 'u'; case CONFIRMED = 'c'; } class User{ public UserType $type; public function __construct(string $type){ // 失敗したらちゃんと例外が上がります $this->type = UserType::from($type); } } $user = new User($_GET['type']); echo $user->type->value;

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

可能性が収束しましたね chatGPTさんもこれにはニッコリ 俺もニッコリ 「なにが起こるのか、そしてtypeがなにをとりうるのか」がわかる まあ、命名が悪いのだが(これはわざとです!!!)

Slide 27

Slide 27 text

はい

Slide 28

Slide 28 text

??「書いた俺はわかるよ!」 たしかその時は動いたのかもしれない 「PHPの気持ちになればわかる」「はい」 print debugすればいいとおもいきや、循環させると var_dump で爆発する xdebugつかってもきびしいことが多々ある

Slide 29

Slide 29 text

辛い例をあげてみましょう 以下は創作です(色々な現場での記憶を混ぜた例)

Slide 30

Slide 30 text

$user = getUser($_GET); // ここでPHP の配列ちゃんが生まれた if(!is_array($user)){ // かっこよく例外にしよう! throw new OutOfBoundsException("user not found"); } $user['lastLoginAt'] = time(); // 最終ログイン時刻更新! saveUser($user); // データ加工したい!でも1 メソッドが長いのは悪いって聞いた!関数化だ! $user = decorateUser($user); // うーん、なんかcreated_at がない事がある…isset いれたらエラー消えた! if(isset($user['created_at']) && (int)$user['created_at'] < 946684800){ $user = oldUser($user); // 古いユーザーは特別な対応が必要なのだ } unset($user['hashed_password']); // セキュリティの都合で消せって言われたから書いた renderProfilePage($user);// ページをレンダリングします!

Slide 31

Slide 31 text

ウッ

Slide 32

Slide 32 text

とりま、getUser()を見てみる function getUser(array $get): array{ $pdo = new PDO('sqlite::memory:'); // サンプルなんで… $stmt = $pdo->prepare("SELECT * FROM users WHERE id=:id"); $stmt->bindValue("id", $get['id']); return $stmt->fetch(PDO::FETCH_ASSOC); } 「なるほど、users テーブルから引いてくるんだね!  …情報ゼロやんけ!DBスキーマをみないといけないのか…」 開発と本番でズレてたりして死ぬなどもありますね

Slide 33

Slide 33 text

生まれた時から無限の可能性を持ちがちなPHPの配列 select * なので、 users テーブルのスキーマを確認する必要がある そもそも入力が $_GET だと「何が送信されてんだっけ…?」となり大変 Json SchemaがあるJSONも、現実に信用できるかは怪しい ウワーッ!!!

Slide 34

Slide 34 text

decolateUser()をみてみる function decorateUser(array $user): array { if( // 久々のログインならバナーをだしたい isset($user['lastLoginAtEpoch']) && $user['lastLoginAtEpoch'] >= time() - 604800 ){ $user['isLongTimeMissed'] = true; } // 開発中は管理者ってことにしておこう! if(TEST_MODE){ $user['is_admin'] = true; } // よくわからないけど、昔からあったのでコピペしてきた foreach($_SESSION['before_user'] as $k => $v){ $user["before_user_" . $k] = $v; // キーを文字列操作で作るのは最強にヤバい } return $user; } // コメントする気もおきないが、しばしば見るコード

Slide 35

Slide 35 text

PHPの配列君、「旅」をして、さらなる高みへ昇りがち PHPの配列を加工していくコードは非常によく見る なにせ、すごく簡単に書けるので コピペしやすいし、初心者でも書けてしまう 他の場所からコピペで持ち込まれて不要なものが増える 「なんとなく追加した!ヨシ!」「動いているのでLGTM!!!!」 日々コードにifがつけたされ、さらに「可能性」が発散する 「この is_admin ってやつ、本当に必要なのかな消したいな…」 => 本当に不要なのかわからなくて消せない どんどん膨れ上がり「とりあえず動く、趣のあるコード」になる

Slide 36

Slide 36 text

「し型ねえコードだな!!」 とにかく実行してみないと、なにがなになのかわからない (そしてあらゆるケースとはあらゆるケースなので、つまりわからない) 頑張って「PHPの配列の仕様」をとりまとめたつもりになったとしても… 仕様書は削除され、経緯のメモは捨てられ、チャットやバグトラのツールは切り替 え時にマイグレーションはされず、コメントはうっかり嘘になり… 空は堕ち、大地は割れ、海は涸れ、コードは読めなくなる

Slide 37

Slide 37 text

現代のソフトウェアは人間&静的解析のチーム戦 すぐ書けるより、正しく読める が重要 「あなた」は一度しか書かないかもしれない しかし、他の「誰か」が何度も読んで、修正する アドホックに修正していくと、小さいほころびが増える 追いかけられなくなり、コピペがはびこる 指数関数的に難易度が上がっていく… 静的解析やAIもPHPの配列相手には無力… 「なのでPHPの配列を捨てようぜ!!!」

Slide 38

Slide 38 text

さりとてPHPの配列は必須 「それをすてるなんてとんでもない(というか無理)」 PHPは「PHPの配列」ありきで成り立っている PHPは「『PHPの配列』を持ったCのWrapper」(主観) 色々な組み込み関数の返値は配列 jsonをデコードしてもPHPの配列になる DBからfetchしてもPHPの配列になる つまり、大体のデータはPHPの配列として生まれる

Slide 39

Slide 39 text

ではどうすれば? PHPの配列に旅をさせるな!! スコープをまたがせない(≒主にメソッドの引数屋返値) if等でのset/unsetをしない(…のはさすがに無理だから、減らす) PHPの配列は中間表現として必要だが、 すぐさま展開し個別の変数にするか、クラスにする ただし、本当の配列( list )は許す(次ページ参照) 最近のPHPは、関数・メソッドの出入り口で可能性を収束できる 型宣言 Type declarations(Type hint) 型宣言された引数 Typed argument 型宣言された返値 Return type 配列を展開し、個別の変数か、クラスにすると信頼性が高い

Slide 40

Slide 40 text

余談: 本当の配列こと、list とは? 0個以上の要素を持ち キー(添え字)が数値の 0 からはじまり、連番で途切れない 例: [1,2,3] , ["a","b","c"] , [new A(1), new A(2)] 順序が数字の通り 値の型が一意(stringならstringしかない) 値が配列の場合は、その配列も同様の条件を満たす であるようなものを指す (実はPHPでもクラスで自作できるが、 ジェネリクスがないので型毎につくる必要がある)

Slide 41

Slide 41 text

余談: 「バリデーションすればいいんじゃないの?」 バリデーション・アサーションは変更が大変 あらゆる境界 にアサーションや定義を追加するのは困難 忘れてもこわれない 「値(≒PHPの配列に相当するなにか)」自体が制約をもっていないときびしい 類似: 「ArrayShapeで静的解析をかければいいんじゃないか?」 人間が無視できる(≒無視する人間が出てくる) 実行時エラーにもならない (AOP等駆使しReflectionでバリデーションとか理解できる人は少ない) 無限の可能性をもつPHPの配列は静的解析でも限界がある

Slide 42

Slide 42 text

ということでPHPの配列を閉じ込めていこうな!

Slide 43

Slide 43 text

閉じ込め方の例、DB編

Slide 44

Slide 44 text

まずはDTOをつくる まず、以下のようなDTOを定義する 名前は別に何でも良い。私は UserStruct とか名付ける事も多い 使われうるプロパティをもつ 不要なフィールド(例: hashed_password )はそもそも持たない たりなければとにかく足す、実行時の追加は許さない class UserDto{ public function __construct( public int $id, public string $name, public int $lastLoginAtEpoch, public bool $isLongTimeMissed = false, public bool $isAdmin = false, ){} }

Slide 45

Slide 45 text

DTOとは? データを転送するためのクラス (私が思う物は正確にはDTOではないのだが、一番ほしい物と概念が近いので、皆さ んがググるキーワードのために、今回DTOとします) (私は「本物のDTO」のようにgetter/setterは必須と思わないし…) 配列より厳格で、静的で、透明性が高く、型の名があり、 静的解析と相性が良い

Slide 46

Slide 46 text

fetchしたら即DTOにする function getUser(int $id): UserDto{ $pdo = new PDO('sqlite::memory:'); $stmt = $pdo->prepare("SELECT id, name, UNIX_TIMESTAMP(last_login_at) as lastLoginAtEpoch FROM users WHERE id = :id"); $stmt->bindValue("id", $id, PDO::PARAM_INT); $stmt->execute(); return new UserDto(...$stmt->fetch(PDO::FETCH_ASSOC)); } 引数にPHPの配列はやめる、 getUser($_GET['id']) など明示する 返値が UserDto であることを明示する 上は0件だと例外が上がる

Slide 47

Slide 47 text

余談: fetchAllの例 // foreach 例 $list = []; while(false !== $row = $stmt->fetch(PDO::FETCH_ASSOC)){ $list[] = new UserDto(...$row); } return $list; // array_map 例 return array_map( fn(array $row) => new UserDto(...$row), $stmt->fetchAll(PDO::FETCH_ASSOC) );

Slide 48

Slide 48 text

余談: そもそも、私ならこの場合 // 1 つ return $stmt->fetch(PDO::FETCH_CLASS, UserDto::class); // 全部 return $stmt->fetchAll(PDO::FETCH_CLASS, UserDto::class); と書くのだが、世間では FETCH_CLASS はあまりなじみがないらしいので…。

Slide 49

Slide 49 text

「私のフレームワークではそう書けないんだけど?」 function getUser(int $id): UserDto{ $user = App\Models\User::findOrFail($id) return new UserDto( (int)$user->id, (string)$user->name, (int)$user->lastLoginAtEpoch(), ); } splat演算子( ...$var )を使わない、PHP<8の配列の場合も同様 上の $user->id が $user['id'] という感じ

Slide 50

Slide 50 text

こういうコードを書くと聞かれるよくある余談 「 UserDto のプロパティはテーブルのカラムと一致していなくていいの?」 良い 一つの「データソース」が、複数の異なる形状のDTOにマップされて良い テーブルでも、JSONでも、 $_POST 等も 「ソースとDTOが1:1!」というのは、Active Record の呪いである(独自考察) ARの発想をすててみましょう 結果として全部が必要な場合もあるし、手抜きでそうすることもある。 実務では UserPublicDto とか UserInternalDto など、派生を作る事が多い Internal は機密情報を持っているが、 Public は無い等が表現できる

Slide 51

Slide 51 text

「たくさん書くの面倒」「理解不能で触れなくなるよりマシでは?」 「PHPの配列と違ってこわれやすい!」「壊れたのが解るのはうれしいが?」 renderProfilePage の引数には UserPublicDto しか受け取れないと記述するなどが でき、安全なデータであると「表現」できたりもする Public という名前のDTOに機密情報を書いたら書けるので、 あくまでも人間が理性的なら、ですが UserPublicDto , AnotherUserPublicDto とか、コピペで増えることもある 手抜きで全部入りのPHPの配列もどきになるよりマシ 静的解析でリファクタすればよい

Slide 52

Slide 52 text

「コンストラクタでなければダメなの?」 ファクトリメソッドでもよい ただし、「ファクトリをたくさんつくればDTOは一個でよい!」のはNG なんでもかんでもファクトリを増やしていくと、 だんだんプロパティに「デフォルト値」なる物が生まれ、 DTOがPHPの配列並に信用できないものに転落しがち。 (理論上、ifがふえているのと同等である) これを私はDDTO(動的なDTO)と呼ぶ… もちろんアンチパターンである、せっかくのDTOが台無し 「デフォルト値」は動的な余地を残すので避けた方が良い なお「リファクタリングできない現場」でしばしば見かける

Slide 53

Slide 53 text

話をもどして、重要なことは getUser のスコープ内部で、配列(や stdClass )が役割を終えていること $array = $stmt->fetch(PDO::FETCH_ASSOC) return new UserDto($array['id'], $array['name']); もし必要なら、この箇所だけPHPの配列を解きほぐせば良い これくらいなら読める

Slide 54

Slide 54 text

DTOになっていてうれしい事 // 返値がかならずUser であることが保証されるので、isset などが不要 // ( 別途、例外処理は必要だが) $user = getUser((int)$_GET['id']); $user = decorateUser($user); $user は、必ず UserDto である どんなプロパティがあって、それらの型がコードでわかる falseやnullではない(無論、0件時のエラーハンドリングは必要) PHPの配列は消えましたね!ヤッター! その後の decorateUser は、必ず UserDto がくることを信頼できる decorateUser 内部でのバリデーション(?)を減らせる (まあ、DTOを加工していく上のコードは微妙…)

Slide 55

Slide 55 text

余談: 返値にT|null パターンもある function getUser(int $id): UserDto|null{ //... $array = $stmt->fetch(PDO::FETCH_ASSOC); if(!is_array($array)){return null;} return new UserDto(...$array); 例外処理が面倒な場合にはこのパターンもある、私も結構好き ただ、できれば「0件だった」例外を定義した方が良いと思う 例外なら、雑に書いたときエラーになるので安全側に倒れている 「if文をへらす設計」に自然となるのでギプスとしても便利 fetchAll なら空配列だろうから問題にならないかと思う

Slide 56

Slide 56 text

加工の様子も見てみましょう (「そもそも加工するな」「ごもっともです」) if( isset($user['lastLoginAtEpoch']) && (int)$user['lastLoginAtEpoch'] >= time() - 604800 ){ $user['isLongTimeMissed'] = 'true'; } if($user->lastLoginAtEpoch >= time() - 604800){ $user->isLongTimeMissed = 'true'; } issetが不要になった(存在が保障されるので) 型キャストが不要になった(intと定義したので)

Slide 57

Slide 57 text

そもそも、必須処理ならHydrate処理をコンストラクタに持つ事も可能 class UserDto{ public bool $isLongTimeMissed; const DELTA_SEC = 604800; public function __construct( public int $id, public string $name, public int $lastLoginAtEpoch, ){ $this->isLongTimeMissed = $this->lastLoginAtEpoch >= time()-self::DELTA_SEC; } }

Slide 58

Slide 58 text

加工時にうれしい事 UserDto 以外がわたされたら、TypeErrorになる decorateUser 内部でプロパティ名をTypoしたらIDEが指摘してくれる 例として、 $user['is_aaadmin']=true は警告されないが、 $user->is_aaadmin = true はプロパティが無いと指摘される $user->lastLoginAtEpoch が存在しintであることが保障されるので、 isset チェッ クやキャストが不要になる

Slide 59

Slide 59 text

余談: 静的解析による不要なプロパティの探索 // 謎の定数からの要素セット if(TEST_MODE){ $user->isAdmin = true; } 配列の時は isAdmin の参照があるかわからなかったのでそっと放置していた… それが、オブジェクトのプロパティならPhpStormのfind usage(静的解析)が使える 結果参照箇所がなければ削除できてHAPPY!!!

Slide 60

Slide 60 text

余談: 動的な箇所があると見落とす /** @var UserDto */ // <== このアノテーションがないと`$user` が何か不明 $user = GodContainer::get("UserDto", 1); if($user->isAdmin){ /*...*/ } 注:コンテナ、サービスロケータ、ファサード等、(明示な)型がないオブジェクト生成器 では見落とす 人がみれば解るが、静的解析ができない。PhpDocは必須 簡単な判断方法:「実装」にコードジャンプができるか? 親クラスやInterfaceにジャンプしたら、静的解析は追跡しきれていない 例「Modelってやつがでてきた」「ヴァーー」

Slide 61

Slide 61 text

さて、最初のコードを振り返る

Slide 62

Slide 62 text

// 必要なメソッドはコンストラクタに移動したよ $user = getPublicUserOrException($_GET['id']); UserService::updateUserLastLoginAtByUserId$user->id); renderProfilePage($user);// ページをレンダリングします! (decorateUserはUserDtoのコンストラクタにおり、消滅したとする)

Slide 63

Slide 63 text

function getUserOrException(array $id): User{ $pdo = new PDO('sqlite::memory:'); $stmt = $pdo->prepare("SELECT * FROM users WHERE id=:id"); $stmt->bindValue("id", $get['id']); // 引けないとTypeError になる、独自例外にWrap しなおすことも多い return $stmt->fetch(PDO::FETCH_CLASS, User::class); }

Slide 64

Slide 64 text

はい というような塩梅です

Slide 65

Slide 65 text

次、APIリクエストのDTO APIリクエスト時には色々パラメタがありそれらを持ち運ぶことがあります (DBでもまあ同様ではあるが) 仮に、GETでユーザーIDを指定すると、ユーザーのポイント情報を取得するAPIがあると します それらのリクエスト・レスポンスにもPHPの配列がつかわれがち 仮に、MethodとUrlがあればとってこれるAPIがあるとして話します

Slide 66

Slide 66 text

重要なこと(あくまで指標です) 「不完全なインスタンス」を作らない リクエストやレスポンスオブジェクトが「生まれる」場所を集約する とりあえず new して、配列のようにいじっていくのはバグの温床 「この先で必要なパラメタが生まれるんだ、見逃してくれ」 作れる状態になるまで引数で連れ回しましょう、 トランザクショナルな制作はバグの温床 namespaceをつかう 「良い名前」をより先に、「違う名前」をつける意識 現代はツールの進化でリネームリファクタリングは簡単にできる (呼び出し側などで動的にしていないかぎり!) 名前は「特性・役割」でなく、「実現する機能」で切る Request\UserPointDto でなく、 UserPoint\RequestDto が良い

Slide 67

Slide 67 text

ダメな例 class SomeResponse { public string $userId; public string $path; } // --- $data = getPointData($userId); // 配列が返るものとする $res = new SomeResponse() $res->userId = $res{'id']; $res->point = $res['point']; new した後にプロパティに代入していくのは悪い手法 中途半端な状態が存在しうると配列と大差がなくなる (どういうプロパティがあるかわかるだけ良いが)

Slide 68

Slide 68 text

レスポンスのDTO例 namespace My\SomeApi\Point; class UserPointStatusResponseStruct { public function __construct( readonly public int $point, readonly public string $expire_at, ){ } } 同様に「一発で完成する」ように作る レスポンスは取り出ししかなく、加工できないようにする 加工したいなら、変数をつくればいいじゃない readonlyをつければ、作成された後に修正ができない (が、セーフガード・補助輪・フールプルーフであり必須ではない)

Slide 69

Slide 69 text

利用例、どうやって new するか デコードしたその場で解体して使う $json_string = json_encode([ // 一旦Stub 'point'=>10, 'expire_at'=>'2024-01-01' ]); $array = json_decode($json_string, true); // ここでValidation をおこなうケースもあるでしょう $userPointStatus = new UserPointStatusResponseStruct( $array['point'], $array['expire_at'] ); // もうarray の役目は終わり PHPの配列はもうすてる、この先は $userPointStatus を信用できる

Slide 70

Slide 70 text

余談: レスポンスのDTO(?)例 その2 namespace My\SomeApi\Point; class UserPointStatusResponseStruct { readonly public DateTimeImmutable $expireAt; public function __construct( readonly public int $point, readonly public string $expire_at, ){ $this->expireAt = new DateTimeImmutable( $expire_at, new DateTimeZone("Asia/Tokyo") ); } } こういうHyderateする仕組みをいれることもある

Slide 71

Slide 71 text

余談: 直接JsonStringを取り込むべきか? class UserPointStatusResponseStruct { readonly public int $point; public function __construct( string $json_string, ){ $data = json_decode($json_string); $this->point = $data['point']; } } // ... $userPointStatus = new UserPointStatusResponseStruct( json_decode($json_string, true) ); たまーに聞かれるが、私は悪いと思う テストがしにくい 「ParseできるJsonか?」の責務はHttpClientが背負った方が良い

Slide 72

Slide 72 text

リクエストのDTO(?)をつくる namespace My\SomeApi\Point; class getUserPointRequestStruct { readonly public string $method = 'GET'; readonly public string $path; public function construct( readonly public int $userId, ){ $this->uri = "http://localhost/point/{$userId}"; } } (上は「良い設計」とは到底いえませんが、短く書きたかったので許せ) リクエストも手続き型的に作らず、中途半端なインスタンスが生まれないように 「使っているHttpClient」の仕様にあわせない方が良い

Slide 73

Slide 73 text

いきなり余談: Http ClientはWrapしたほうが良い…かも? 配列でオプションをわたすライブラリは仕様が変わることがある、Guzzleとか 仕様がかわると、詰め替え箇所を全部変える必要がある 一層Wrapすることを検討してみてください Wrapperは、愚直に詰め替えを書いて良い、むしろそれくらいで良い 「あるライブラリを満足させる」ために設計しない方が良い 解るなら、PSR-7とか使っても良いとおもいます

Slide 74

Slide 74 text

使い方 たとえばGuzzle class ClientWrapper { static public function getUserPointStatus( getUserPointRequestStruct $req ) :UserPointStatusResponseStruct { $client = new GuzzleHttp\Client(); $array = $client->request( $req->method, $req->uri, )->json(); return new UserPointStatusResponseStruct( $array['point'], $array['expired_at'], ); // Array はここで終了 } }

Slide 75

Slide 75 text

使い方2 「Guzzleは嫌だなあ」 class ClientWrapper { static public function getUserPointStatus( getUserPointRequestStruct $req ) :UserPointStatusResponseStruct { $json_str = file_get_contents($req->uri); $array = json_decode($json_str, true); return new UserPointStatusResponseStruct( $array['point'], $array['expired_at'], ); // Array はここで終了 } } ※ 本当はエラーハンドリングしてくれよな

Slide 76

Slide 76 text

はい

Slide 77

Slide 77 text

あれ?なんでDBとAPIはちがうの? DBは、良いか悪いか別として、加工して戻すことがある (不変にし、保存用のDTOに詰め替えることもある) APIは加工することなどありえない(…よね?) そのような差があると考えている まあ、安定性のためなら、不変が一番だとおもいます (しかし、安定性を追求するにも、さすがに俺も面倒だな…) 「要はバランス」

Slide 78

Slide 78 text

ということで 配列を旅させないのは簡単です なお、こういうのは「面倒だな」という思想もあります 有名なフレームワークとかそういうかんじです そして「中途半端な」ものを作らないのもむずかしくはない(はず) だが、みんな配列でそだったので、やりがち。 ちゃんとできてると安全 テストやエラーハンドリングもしやすい(はず) どんどんクラスふやしていこう!!!(?)

Slide 79

Slide 79 text

余談: 無名クラス どうしてもクラスファイルを作りたくない時がまれにある 無名クラスというものがある 「キー名を限定できる配列」としてつかうと配列よりはマシな場合もまれにある しかし名前がないと型がないので、旅にはつかえないと思った方が良い $anonymous_dto = new class(1){ public function __construct( readonly public int $id, ){} }; => object(class@anonymous)#3 (1) { ["id"]=> int(1) }

Slide 80

Slide 80 text

「もっと…こう…OOPっていうか…抽象化…できそうじゃない?」 「あれ、これってRequestの親をつくり、継承やInterface化して、Wrapper の APIをそ ろえればなお良さそう」という悪魔のささやきは聞こえましたか? それは地獄の釜ですよ、まだ引き返せます 「一箇所直せば直る」のは魅力的ですが、それに引きずられることがある 10個とかなら、全文検索して直すほうが早くて安全な事も多い 「ミスするかも…」「それ静的解析にレビューさせた?」 「私は」PHPの配列をできるだけ排除するだけで十分だと思います が、最初に書いた通り、自分で決断しような!

Slide 81

Slide 81 text

ぶっちゃけOOPとか考えない方が良い 本トークはクラスで書いたけど、こんなのはOOPとは言わない、 構造体に毛が生えたような発想です PHPの配列を排除しようとすると、経験上 「ファイル数や、コード行数が増える」 「(PhpDocがないと型が同定できない)動的な部分が増える」 「アノテーションやAttributeや継承を駆使した『魔法』が増える」 上記どれかが発生しやすい 今回は「ファイルや行数を増やす」にした なぜなら素朴で、初心者にも書きやすくて、ルールにしやすくて、 静的解析がききやすいので、リファクタリングがしやすいから 九割のケースにおいて、構造化プログラミングもどきで十分です 素朴かつ疎にしましょう、KISS、YAGNI、POPO

Slide 82

Slide 82 text

今回のサンプルコードは新しめ機能もありますが 古来のコードでも十分生かせる 新しいPHPは多少補助輪が増えたとおもえばよい readonly やスカラ型はうれしいが、人間が気をつければよい PHP4でも DTOは書けるし、Annotationをがんばれば、 PhpStorm/PHPStanはちゃんと解析して指摘してくれるよ!

Slide 83

Slide 83 text

I am NOT smarter than static analytics! PHPの配列を捨てると静的解析という味方がつく 静的解析は、人間より間違いにめざとい 何度解析させても即座に、文句なく、間違いを淡々と指摘してくれる 静的解析で見つかる程度の間違い探しに、同僚の時間を奪う必要は無い そのために、静的解析に寄り添ったコードにするのは理にかなっている

Slide 84

Slide 84 text

まとめ 「とりあえず配列」はとりあえずやめよう (いわゆる list は許容) PHPの配列には旅をさせない 一つの回答としてのDTO(的なもの)がある 本質としては静的解析が効かせることが一番重要 99%の人間より静的解析は賢い 完 質問ありますか?