Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPの最高機能、配列を捨てよう!! / Throw away all PHP array now!!!

uzulla
March 25, 2023

PHPの最高機能、配列を捨てよう!! / Throw away all PHP array now!!!

At: PHPerKaigi 2023 ( https://phperkaigi.jp/2023/ ) Track A
DateTime: 2023/3/25 10:20 (40min)
Speaker: uzulla

uzulla

March 25, 2023
Tweet

More Decks by uzulla

Other Decks in Technology

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. ハッシュ的な探索
    //
    初期化は不要
    $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!

    View full-size slide

  9. 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)

    View full-size slide

  10. 柔軟な多次元操作、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);

    View full-size slide

  11. 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" }}}}

    View full-size slide

  12. ルーター
    $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
    と表示

    View full-size slide

  13. 「生きている」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);

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 突然、書き換えてみる
    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;

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. $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);//
    ページをレンダリングします!

    View full-size slide

  25. とりま、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スキーマをみないといけないのか…」
    開発と本番でズレてたりして死ぬなどもありますね

    View full-size slide

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

    View full-size slide

  27. 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;
    } //
    コメントする気もおきないが、しばしば見るコード

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 閉じ込め方の例、DB編

    View full-size slide

  37. まずは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,
    ){}
    }

    View full-size slide

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

    View full-size slide

  39. 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件だと例外が上がる

    View full-size slide

  40. 余談: 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)
    );

    View full-size slide

  41. 余談: そもそも、私ならこの場合
    // 1

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

    View full-size slide

  42. 「私のフレームワークではそう書けないんだけど?」
    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']
    という感じ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  48. 余談: 返値に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
    なら空配列だろうから問題にならないかと思う

    View full-size slide

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

    View full-size slide

  50. そもそも、必須処理なら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;
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. 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);
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  62. 利用例、どうやって 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
    を信用できる

    View full-size slide

  63. 余談: レスポンスの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する仕組みをいれることもある

    View full-size slide

  64. 余談: 直接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が背負った方が良い

    View full-size slide

  65. リクエストの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」の仕様にあわせない方が良い

    View full-size slide

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

    View full-size slide

  67. 使い方 たとえば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
    はここで終了
    }
    }

    View full-size slide

  68. 使い方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
    はここで終了
    }
    }

    本当はエラーハンドリングしてくれよな

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    質問ありますか?

    View full-size slide