今回のクイズはLaravelクイズの第3弾

mpyw氏:「Laravelクイズ ~Qiita Night PHP 2023 Winter~」ということで始めます。よろしくお願いします。今回は「Qiita」のスライドを使わせてもらっています。(倍率が)100パーセントだと見にくかったので、50パーセントでだいぶ縮小して表示しています。自己紹介をしていると時間がないので、さっさと始めていきます。

以前の問題。最初に作ったLaravelクイズがあって、それに続き弊社の@fuwaseguくんが作ってくれた問題があって、(さらに)それに続いて第3弾ということで、今回また私から出題させてもらった内容になります。

以前から「Twitter」上、「X」上でこの問題については共有していたので、それのおさらいというか。今回初めて見る方もいると思いますが、その方向けにけっこう丁寧に説明しながら、知っている方はおさらいという感じで見てもらえれば幸いです。

第1問 非常に危うい動作をするものはどれ?

じゃあ進めていきます。第1問。PUT users/というエンドポイントでUserというモデルの更新を処理する時、更新対象を引数の$userという変数名で指定された型Userの変数で判断し、更新内容のPOSTボディを連想配列のクエリString、userという中身の中にnameというキーを持っているやつですね。(user[name])を引数の$requestで受ける時に、以下のうち正しくPOSTボディを取り出せない可能性があるものはどれでしょう。

1番がプロパティアクセスでuserを取り出す。2番がget()というメソッドを使って取り出す。3番がinput()というメソッドを使って取り出す。4番がall()というメソッドを使って取り出したあと、userというキーにアクセスする。

1個だけ非常に危うい動作をするものがあります。5秒ぐらい待ちます。

ちょっと時間もないので発表にいきます。正解は1番ですね。$request->userでアクセスをすると非常に危険です。正しく内容がPOSTされた時は$request->input('user')と等価の動作を取るんですが、そのリクエストの$requestペイロードの中にuserというものが存在しない時、なんとこのコントローラーの引数にあるUser $userの値を取りにいってしまいます。

リクエストの中身かもしれないし、そのモデルそのもののオブジェクトを取ってしまうかもしれないということで、ぜんぜん違うものを取ってしまう可能性があるので、この書き方はできれば使わないようにしてほしいです。これは以前に私がQiitaで書かせてもらった記事のおさらいになります。

他のものに関して軽く説明しておくと、2番の$request->get('user')はたぶん見たことがある方はほとんどいないかと思いますが、一応Laravelでちゃんと存在しています。中身は(スライドに)書かせてもらったとおり、Symfonyのベースとなっているリクエストを継承しているだけの処理になります。

Laravelでよくある、JSON形式を解釈してドットチェインでキーを指定して中身を取り出すという動作をとることができないので、実質ちょっと使い勝手が悪い。$_POSTしかハンドリングができないので実用性はちょっと薄いかなということで、豆知識程度に覚えておいてください。

$request->input('user')に関しては説明不要ですね。もっともよく使われるものです。とりあえずこれを使っておけば困らないと思います。$request->all()に関しては同等の動きなので、特に説明はいらないかなと思います。

第2問 使い方が間違っているものはどれ?

じゃあ次にいきます。2問目。Eloquent Modelのスコープ機能に関して、以下のうち使い方が間違っているものが1つだけあります。

1番。グローバルスコープという機能に関して、そのapply()メソッドで受けた$queryという変数にそのままWHEREとかをチェインして条件を付与していく。2番が、新しいクエリインスタンスを作り直して、それをリターンする。既存のオブジェクトを編集するというよりは新しいものをリターンするという動きですね。3番、4番がそれのローカルスコープ版になります。

次のうち、1個だけ動作として間違っているものがあります。じゃあ5秒ほど待ちます。

じゃあ正解を発表します。正解は2番です。グローバルスコープクラスに関しては、返り値がインターフェイス上でvoidと記述されているのでちょっと不便ですが、これに逆らうことはできないです。

どうしてもインスタンスごと入れ替えたいような処理があったら、ローカルスコープを使って実装してもらう必要があるということになります。

第3問 Eloquent Builderの機能に関して間違った説明をしているものは?

じゃあ次に3問目にいきます。これは時事問題ということで、私が最近X上で何回かつぶやいていた内容になるんですが、いきます。

Eloquent Builderの機能に関して、間違った説明をしているものはどれでしょう?

1番。createOrFirst()というメソッドは、まずINSERTしてみて、ユニークキー制約エラーになった時だけSELECTするという動きをする。2番。firstOrNew()は、まずSELECTしてみて、結果があればそれをインスタンス化、なければ空のPHPのオブジェクトでそのままモデルクラスをNEWするという動きをする。3番。firstOrCreate()は、まずSELECTしてみて、結果が見つからなかった時だけINSERTする。4番。updateOrCreate()は、まずUPDATEしてみて空振りした時だけ、要するにそのアップデートが走らなかった時だけINSERTする。

1つだけ間違っているものがあります。どれでしょうか。

発表にいきます。正解は4番です。updateOrCreate()が間違っています。updateOrCreate()は、最初にupdateという名前から始まっていますが、まずはSELECTクエリが投げられます。SELECTした結果があったら、それを基にUPDATEという処理が走って、なければINSERTされます。だから実際の動きはfirstOrCreate()みたいな名前が妥当というわけですが、省略したような名前になっていますね。

1番のcreateOrFirst()は聞き慣れない方がいるかもしれないですが、Laravel 10xで追加された機能になります。確かLaravel 10.20ぐらいだったかな? わりと最近の機能です。

これは宣伝になるんですが、createOrFirst()に加えて、firstOrCreate()とupdateOrCreate()に関しても以前は使われていなくて、シンプルにcreate()を使っていました。10.29以降は内部的にcreateOrFirst()を再利用するようになっていて、これによって、並列実行された時、例えばレプリケーション遅延とかが起こっていたとしても、安全なフォールバック動作を取ってくれるようになっています。

ということで、firstOrCreate()、updateOrCreate()の実用性が、10.29以降は以前よりも上がっていると思ってもらってかまいません。

第4問 タスクスケジューリング機能に関して間違った説明をしているものは?

では4問目にいきます。タスクスケジューリング機能に関して、間違った説明をしているものはどれでしょう。

1番。withoutOverlapping()とonOneServer()という機能がどっちもあるんですが、ともにタスクの重複実行を防ぐための機能である。2番。バッチサーバーが複数台構成の場合、必ずしもこれらを両方使わなければならないとは限らない。どっちかだけで十分ということですね。3番。複数台構成の場合、これらを使うためには必ずキャッシュドライバとして、ファイルじゃなくて、RedisとかMemcachedといったKVSを使用しなければならない。4番。withoutOverlapping()は単一台構成の場合に使っても意味がある。

タスクスケジューリング機能は使っていない方もいるかもしれないですが、ご了承ください。どれでしょうか。

正解は3番ですね。複数台構成の場合、これらを使うためには必ずキャッシュドライバとしてKVSを使用しなければならない。実用上、KVSを使うことがほとんどですが、これは引っかけ問題で、実はKVSを用意せずともリレーショナルデータベース、MySQLとかPostgreSQLを使ってもらってOKです。

ローカルファイルを使うと、そのインスタンス内だけでしか共有されないんですが、共有されるものを選べばなんでもいいので。KVSじゃなくてRDBでもOKという引っかけ問題ですね。

このあたりに関しては、以前のQiitaの記事で説明させてもらっています。

(スライドを示して?)このあたりは補足ですが、時間がないので端折ります。

第5問 テストで使用できるトレイトについて間違った説明をしているものは?

5問目。テストで使用できるトレイトについて、間違った説明をしているものはどれ? 

1番。DatabaseMigrationsはテストメソッド開始ごとに毎回全テーブルをDROPして作り直しを行う。2番。DatabaseTransactionsはテストメソッド開始ごとにトランザクションを開始し、テストメソッド終了時にロールバックを行う。3番。DatabaseTruncationはテスト全体で1回だけ、全テーブルの全行削除を行う。4番。RefreshDatabaseはテスト全体で1回だけ、テーブルのDROPと作り直しを行う。

どれでしょうか。この中でたぶん一番有名なのがRefreshDatabaseだと思いますが、それ以外にもたくさん存在しています。

では正解にいきます。間違っているものはどれでしょう。3番です。DatabaseTruncationはテストメソッド開始ごとが正解になります。DatabaseTransactionsとかRefreshDatabaseのようにテスト用トランザクションが生えないので、比較的実操作に近いテストが可能です。

ということで、個人的にはこれはRefreshDatabaseよりも今はDatabaseTransactionsを使ったほうがいいんじゃないかなというぐらいで、わりと推しているトレイトになります。

(スライドを示して)一応他の説明も書いてあります。もうあと1分しかないので飛ばしていきます。

第6問 UUIDについて間違った説明をしているものは?

6問目。UUIDについて間違った説明をしているものはどれでしょう。

1番。Strクラスのuuid()というメソッドはデフォルトでUUID v4を返す。2番。orderedUuid()はUUID標準に準拠していない。3番。uuid()もしくはorderedUuid()というメソッドはそれぞれ別々にロジックをカスタムすることができる。4番。orderedUuid()は、2059年問題みたいな生成スペース枯渇に関する問題を抱えている。

1つだけ間違った説明があります。

では正解にいきますかね。これが最後の問題です。3番です。uuid()、orderedUuid()のロジックをそれぞれ別にカスタムすることはできません。非常に残念な実装になっているんですが、createUuidsUsing()というメソッドで、どちらもここで設定したクロージャ―を使うような動きになってしまいます。非常に残念な実装です。なぜそうなっているかは私にもわかりません。

uuid()に関してはデフォルトのUUID v4が返ってきますね。orderedUuid()はLaravelがオレオレで用意したものであって、UUID標準は無視しています。

2059年問題があるということに関しても正しいです。なので、長期的に動作をするソフトウェアを作る場合はorderedUuid()は使わずに、整列されているUUIDで良ければUUID v7というものを自分で用意したほうがいいかなと考えています。

ちょうど時間になったので、これで終わりにします。