本スライドはフルスクリーン,倍率 50% での閲覧を推奨します
自己紹介


通算 Qiita 9 位らしい。
(最近あんまり書いてなくてごめんね…)
今日のテーマ
Laravel クイズ ~For Qiita Night 2023 Winter~
以前の問題
by @mpyw
by @fuwasegu
本日の問題
本日のものは第 3 弾です!
シンキングタイムを挟んだ上でサクサク進めていきましょう
1問目: 問題
PUT users/<id>
というエンドポイントで User
モデルの更新を処理する。更新対象を引数 User $user
で判断し,更新内容の POST ボディ user[name]=...
を引数 Request $request
で受ける。以下のうち,正しく POST ボディを取り出せない可能性があるものはどれ?
※application/json
ではなく, HTML フォーム送信に使われる application/x-www-form-urlencoded
です
public function put(User $user, Request $request): mixed
{
dump($request->user); // 1
dump($request->get('user')); // 2
dump($request->input('user')); // 3
dump($request->all()['user']); // 4
}
1問目: 回答
正解: 1
$request->user
- 正しく内容が POST されたときは
$request->user
は$request->input('user')
と等価です。 - 内容が欠落した不正なリクエストでは, ルートモデルバインドされた
User $user
引数にフォールバックされてしまいます。
public function put(User $user, Request $request): mixed
{
// $request->input('user') かもしれないし, $user かもしれない!
dump($request->user);
}
この挙動に関しては過去の記事で解説しています。
誤り: 2, 3, 4
$request->get('user')
正しいです。Laravel ではほとんど使われることがありませんが,ドットチェイン展開処理などを行わない素の取り出し処理として get()
は存在しています。但し $_POST
しかハンドリングしてくれないので, JSON 形式のリクエストを取り扱うことはできません。
/**
* This method belongs to Symfony HttpFoundation and is not usually needed when using Laravel.
*
* Instead, you may use the "input" method.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get(string $key, mixed $default = null): mixed
{
return parent::get($key, $default);
}
$request->input('user')
正しいです。最もよく使われるメソッドです。ドットチェインを展開する機能を持っており, $_POST
に入ってきたクエリーストリング形式の文字列,および file_get_contents('php://input')
に入ってきた JSON 形式の文字列をどちらも処理できます。
$request->input('user.name')
$request->all()['user']
正しいです。一度配列として $this->input()
で得られる全要素を取り出していますが,こちらは User $user
へのフォールバックは含めていないため,安全です。
「実は $_GET
$_POST
が混ざっちゃうから正解複数あるのでは?」とご指摘がありましたが,それはその通りです。これは問題の不備でした。正確には 「意図せずリクエストに由来しない値を使ってしまう可能性があるものはどれか?」 という問題のほうが良かったかもしれません。
2問目: 問題
Eloquent Model のスコープ機能に関して,以下のうち使い方が間違っているものはどれ?
- グローバルスコープクラスの
apply()
メソッドで受けた$query
に WHERE 条件を付与 - グローバルスコープクラスの
apply()
メソッドで受けた$query
をサブクエリとしてラップした新しいクエリをリターン - ローカルスコープメソッドで受けた
$query
に WHERE 条件を付与 - ローカルスコープメソッドで受けた
$query
をサブクエリとしてラップした新しいクエリをリターン
// グローバルスコープクラス
class ActiveScope implements Scope
{
public function apply(Builder $query, Model $model)
{
// 1: $query に WHERE 条件を付与
// 2: $query をサブクエリとして使う新しい Builder インスタンスをリターン
}
}
class UserModel extends Model
{
// ローカルスコープメソッド
public function scopeActive(Builder $query)
{
// 3: $query に WHERE 条件を付与
// 4: $query をサブクエリとして使う新しい Builder インスタンスをリターン
}
}
2問目: 回答
正解: 2
グローバルスコープクラスの
apply()
メソッドで受けた$query
をサブクエリとしてラップした新しいクエリをリターン
グローバルスコープの返り値は void
として宣言されているため,これに逆らうことはできません。
interface Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model);
}
誤り: 1, 3, 4
ローカルスコープメソッドで受けた
$query
をサブクエリとしてラップした新しいクエリをリターン
正しいです。ローカルスコープは以下のように処理されます。返り値があるときはそれを使い,無いときは $this
(Builder
自分自身) をリターンしたと見なす実装になっています。
protected function callScope(callable $scope, array $parameters = [])
{
array_unshift($parameters, $this);
$query = $this->getQuery();
/* ... */
$result = $scope(...$parameters) ?? $this; // ←
/* ... */
return $result;
}
3問目: 問題
Eloquent Builder の機能について,間違った説明をしているものはどれ?
-
createOrFirst()
は,まず INESRT してみて,ユニークキー制約エラーになったときだけ SELECT する -
firstOrNew()
は,まず SELECT してみて,結果があればそれをインスタンス化,なければ空の内容でインスタンス化する -
firstOrCreate()
は,まず SELECT してみて,結果が見つからなかったときだけ INSERT する -
updateOrCreate()
は,まず UPDATE してみて,空振りしたときだけ INSERT する
3問目: 回答
正解: 4
updateOrCreate()
は,まず UPDATE クエリを投げて,空振りしたときだけ INSERT する
updateOrCreate()
は,まず SELECT クエリを投げてみて,結果があればそれをもとに UPDATE,なければ INSERT する
という動きになります。実際の動きは firstOrUpdateOrCreate()
相当というわけですね。大変ややこしい…
誤り: 1, 2, 3
createOrFirst()
は,まず INESRT してみて,ユニークキー制約エラーになったときだけ SELECT する
正しいです。Laravel 10.x で最近追加された機能です。
補足
firstOrCreate
updateOrCreate
に関しては内部的に createOrFirst
を使用するように一度改修が入ったのですが,バグが多かったためリバートされ,最終的にバージョン 10.29 以降で復活しています!
4問目: 問題
タスクスケジューリング機能に関して,間違った説明をしているものはどれ?
-
withoutOverlapping()
onOneServer()
は,ともにタスクの重複実行を防ぐための機能である - 複数台構成の場合,必ずしも
withoutOverlapping()
onOneServer()
を両方使わなければならないとは限らない - 複数台構成の場合,
withoutOverlapping()
onOneServer()
を使うためには,必ずキャッシュドライバとして Redis や Memcached などの KVS を使用しなければならない -
withoutOverlapping()
は,単一台構成の場合にも機能する
$schedule->command('foo:bar')->everyMinute()->onOneServer()->withoutOverlapping();
4問目: 回答
正解: 3
複数台構成の場合,
withoutOverlapping()
onOneServer()
を使うためには,必ずキャッシュドライバとして Redis や Memcached などの KVS を使用しなければならない
ローカルファイルに記録すると動作記録が複数台の間でシェアできないので,基本的には KVS を使うのが正解ですが…
普通に RDB を使って OK です。 KVS は必須要件ではないので引っ掛け問題でした
以下で詳しく解説しています。
誤り: 1, 2, 4
withoutOverlapping()
onOneServer()
は,ともにタスクの重複実行を防ぐための機能である
正しいです。
-
withoutOverlapping()
は 実行中の時間範囲 が重複することを防ぐ機能 -
onOneServer()
は あるタイミングにおける実行開始処理 が重複することを防ぐ機能
観点は違いますが,どちらもタスクの重複実行を防ぐための機能です。
複数台構成の場合,必ずしも
withoutOverlapping()
onOneServer()
を両方使わなければならないとは限らない
正しいです。たとえば「サーバ上のローカルファイルキャッシュを掃除する」といったタスクの場合は, onOneServer()
は不要で, withoutOverlapping()
だけの制御で十分かもしれません。
withoutOverlapping()
は,単一台構成の場合にも機能する
正しいです。「前のタスクが終わっていないときに次の実行開始を見送る」という制御は,単一台でも成立します。
5問目: 問題
テストで使用できるトレイトについて,間違った説明をしているものはどれ?
-
DatabaseMigrations
は,テストメソッド開始毎に毎回全テーブルをドロップして作り直しを行う -
DatabaseTransactions
は,テストメソッド開始毎にトランザクションを開始し,テストメソッド終了時にロールバックを行う -
DatabaseTruncation
は,テスト全体で 1 回だけ,全テーブルの全行削除を行う -
RefreshDatabase
は,テスト全体で 1 回だけ,テーブルのドロップと作り直しを行う
5問目: 回答
正解: 3
DatabaseTruncation
は,テスト全体で 1 回だけ,全テーブルの全行削除を行う
テスト全体ではなく テストメソッド開始毎 が正解です。
DatabaseTransactions
RefreshDatabase
のようにテスト用トランザクションが生えないので,比較的実動作に近いテストが可能です。トランケート処理はそこまで負荷が高くないので実用性も問題ないでしょう。
誤り: 1, 2, 4
DatabaseMigrations
は,テストメソッド開始毎に毎回全テーブルをドロップして作り直しを行う
正しいです。非常に重い処理ゆえに,基本的にこれが使われることは無いでしょう。
DatabaseTransactions
は,テストメソッド開始毎にトランザクションを開始し,テストメソッド終了時にロールバックを行う
正しいです。テストごとにトランザクションがロールバックされる,効率的な実行方法です。但しテストの都合によってトランザクションが1個追加されてしまうため,場合によっては DatabaseTruncation
を使わなければならないシーンがあるでしょう。
RefreshDatabase
は,テスト全体で 1 回だけ,テーブルのドロップと作り直しを行う
正しいです。おそらく最も使用率の高いトレイトです。以下のような動きをします。
- 全体で 1 回だけ
DatabaseMigrations
相当の処理を実行 - テストメソッド開始毎に
DatabaseTransactions
相当の処理を実行
6問目: 問題
UUID について,間違った説明をしているものはどれ?
-
Str::uuid()
はデフォルトでは UUID v4 を返す -
Str::orderedUuid()
は UUID 標準に準拠していない -
Str::uuid()
Str::orderedUuid()
の生成ロジックを,それぞれ別々にカスタムすることができる -
Str::orderedUuid()
の生成スペースは 2059 年に枯渇する
6問目: 回答
正解: 3
Str::uuid()
Str::orderedUuid()
の生成ロジックを,それぞれ別々にカスタムすることができる
カスタムはできますが,残念ながら 1 つしか指定することはできません。
(debug_backtrace()
で呼び出し元を識別すれば使い分けることはできるかも )
Str::createUuidsUsing(function (): string {
return /* ... */;
});
誤り: 1, 2, 4
Str::uuid()
はデフォルトでは UUID v4 を返す
正しいです。例えば単調増加な UUID v7 に切り替えたい場合は, Str::createUuidsUsing()
でカスタムする必要があります。
Str::orderedUuid()
は UUID 標準に準拠していない
正しいです。
- UUID v4 の上位 48 ビットをタイムスタンプベースの値に書き換えて使用しています。精度は 10 マイクロ秒 です。
- UUID v7 の精度は 1 ミリ秒 であるため,互換性がありません。
- 任意精度に対応させるためには UUID v8 を使えばよいですが, バージョンビットを v4 にして使っているので,規格標準要件は満たしていません。
Str::orderedUuid()
の生成スペースは 2059 年に枯渇する
正しいです。 10 マイクロ秒精度という無駄遣いが行われているので,意外と早く枯渇します。