Laravel クイズ ~Qiita Night PHP 2023 Winter~

本スライドは**フルスクリーン**，倍率 **<font color=red>50%</font>** での閲覧を推奨します

---

## 自己紹介

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/4c18fe94-4bb9-d814-6601-536d26afcac8.png" width="1700">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/beffa6f1-bf65-f7fc-3ce1-a329ca866cfc.png" width="500">

通算 Qiita 9 位らしい。<br>（最近あんまり書いてなくてごめんね…）

---

# 今日のテーマ<br><br>Laravel クイズ ~For Qiita Night 2023 Winter~

---

## 以前の問題

https://quiz-maker.site/quiz/play/1QTtDt20201213235747

by @mpyw

https://quiz-maker.site/quiz/play/qcMPyf20221101104330

by @fuwasegu

## 本日の問題

https://quiz-maker.site/quiz/play/9uvjbY20231003180725

本日のものは第 3 弾です！
シンキングタイムを挟んだ上でサクサク進めていきましょう

---

## 1問目: 問題

`PUT users/<id>` というエンドポイントで `User` モデルの更新を処理する。更新対象を引数 `User $user` で判断し，更新内容の POST ボディ `user[name]=...` を引数 `Request $request` で受ける。以下のうち，正しく POST ボディを<ins>取り出せない</ins>可能性があるものはどれ？

<font color=green>※`application/json` ではなく， HTML フォーム送信に使われる `application/x-www-form-urlencoded` です</font>

```php
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')` と等価です。
- **内容が欠落した不正なリクエストでは， <font color=red>ルートモデルバインドされた `User $user` 引数にフォールバック</font>されてしまいます。**

```php
public function put(User $user, Request $request): mixed
{
    // $request->input('user') かもしれないし， $user かもしれない！
    dump($request->user);
}
```

この挙動に関しては過去の記事で解説しています。

https://qiita.com/mpyw/items/d26f0ffac1529399a35f

---

#### 誤り: 2, 3, 4

> `$request->get('user')`

正しいです。Laravel ではほとんど使われることがありませんが，ドットチェイン展開処理などを行わない素の取り出し処理として `get()` は存在しています。**但し `$_POST` しかハンドリングしてくれないので， JSON 形式のリクエストを取り扱うことはできません。**

```php
/**
 * 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 形式の文字列をどちらも処理できます。

```php
$request->input('user.name')
```

> `$request->all()['user']`

正しいです。一度配列として `$this->input()` で得られる全要素を取り出していますが，こちらは `User $user` へのフォールバックは含めていないため，安全です。

:::note warn
「実は `$_GET` `$_POST` が混ざっちゃうから正解複数あるのでは？」とご指摘がありましたが，それはその通りです。これは問題の不備でした。正確には **「意図せずリクエストに由来しない値を使ってしまう可能性があるものはどれか？」** という問題のほうが良かったかもしれません。
:::

---

## 2問目: 問題

Eloquent Model のスコープ機能に関して，以下のうち使い方が<ins>間違っている</ins>ものはどれ？

1. グローバルスコープクラスの `apply()` メソッドで受けた `$query` に WHERE 条件を付与
2. グローバルスコープクラスの `apply()` メソッドで受けた `$query` をサブクエリとしてラップした新しいクエリをリターン
3. ローカルスコープメソッドで受けた `$query` に WHERE 条件を付与
4. ローカルスコープメソッドで受けた `$query` をサブクエリとしてラップした新しいクエリをリターン

```php
// グローバルスコープクラス
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` として宣言されているため，これに逆らうことはできません。

```php
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` 自分自身) をリターンしたと見なす実装になっています。

```php
protected function callScope(callable $scope, array $parameters = [])
{
    array_unshift($parameters, $this);

    $query = $this->getQuery();

    /* ... */

    $result = $scope(...$parameters) ?? $this; // ←

    /* ... */

    return $result;
}
```

---

## 3問目: 問題

Eloquent Builder の機能について，<ins>間違った</ins>説明をしているものはどれ？

1. `createOrFirst()` は，まず INESRT してみて，ユニークキー制約エラーになったときだけ SELECT する
2. `firstOrNew()` は，まず SELECT してみて，結果があればそれをインスタンス化，なければ空の内容でインスタンス化する
3. `firstOrCreate()` は，まず SELECT してみて，結果が見つからなかったときだけ INSERT する
4. `updateOrCreate()` は，まず UPDATE してみて，空振りしたときだけ INSERT する

---

## 3問目: 回答

#### 正解: 4

> `updateOrCreate()` は，まず UPDATE クエリを投げて，空振りしたときだけ INSERT する

`updateOrCreate()` は，**<font color=red>まず SELECT クエリを投げてみて</font>**，結果があればそれをもとに UPDATE，なければ INSERT する

という動きになります。実際の動きは `firstOrUpdateOrCreate()` 相当というわけですね。大変ややこしい…

#### 誤り: 1, 2, 3

> `createOrFirst()` は，まず INESRT してみて，ユニークキー制約エラーになったときだけ SELECT する

正しいです。Laravel 10.x で最近追加された機能です。

### 補足

`firstOrCreate` `updateOrCreate` に関しては内部的に `createOrFirst` を使用するように一度改修が入ったのですが，バグが多かったためリバートされ，最終的にバージョン **<font color=red>10.29</font>** 以降で復活しています！

https://zenn.dev/mpyw/articles/laravel-v10-create-or-first

---

## 4問目: 問題

タスクスケジューリング機能に関して，<ins>間違った</ins>説明をしているものはどれ？

1. `withoutOverlapping()` `onOneServer()` は，ともにタスクの重複実行を防ぐための機能である
2. 複数台構成の場合，必ずしも `withoutOverlapping()` `onOneServer()` を両方使わなければならないとは限らない
3. 複数台構成の場合， `withoutOverlapping()` `onOneServer()` を使うためには，必ずキャッシュドライバとして Redis や Memcached などの KVS を使用しなければならない
4. `withoutOverlapping()` は，単一台構成の場合にも機能する

```php
$schedule->command('foo:bar')->everyMinute()->onOneServer()->withoutOverlapping();
```

---

## 4問目: 回答

#### 正解: 3

> 複数台構成の場合， `withoutOverlapping()` `onOneServer()` を使うためには，必ずキャッシュドライバとして Redis や Memcached などの KVS を使用しなければならない

ローカルファイルに記録すると動作記録が複数台の間でシェアできないので，基本的には KVS を使うのが正解ですが…
**普通に RDB を使って OK です。** KVS は必須要件ではないので引っ掛け問題でした

以下で詳しく解説しています。

https://qiita.com/mpyw/items/15d14d920250a3b9eb5a

---

#### 誤り: 1, 2, 4

> `withoutOverlapping()` `onOneServer()` は，ともにタスクの重複実行を防ぐための機能である

正しいです。

- `withoutOverlapping()` は **<font color=red>実行中の時間範囲</font>** が重複することを防ぐ機能
- `onOneServer()` は **<font color=red>あるタイミングにおける実行開始処理</font>** が重複することを防ぐ機能

観点は違いますが，どちらもタスクの重複実行を防ぐための機能です。

> 複数台構成の場合，必ずしも `withoutOverlapping()` `onOneServer()` を両方使わなければならないとは限らない

正しいです。たとえば「サーバ上のローカルファイルキャッシュを掃除する」といったタスクの場合は， `onOneServer()` は不要で， `withoutOverlapping()` だけの制御で十分かもしれません。

> `withoutOverlapping()` は，単一台構成の場合にも機能する

正しいです。「前のタスクが終わっていないときに次の実行開始を見送る」という制御は，単一台でも成立します。

---

## 5問目: 問題

テストで使用できるトレイトについて，<ins>間違った</ins>説明をしているものはどれ？

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

---

## 5問目: 回答

#### 正解: 3

> `DatabaseTruncation` は，テスト全体で 1 回だけ，全テーブルの全行削除を行う

テスト全体ではなく **<font color=red>テストメソッド開始毎</font>** が正解です。

`DatabaseTransactions` `RefreshDatabase` のようにテスト用トランザクションが生えないので，比較的実動作に近いテストが可能です。トランケート処理はそこまで負荷が高くないので実用性も問題ないでしょう。

---

#### 誤り: 1, 2, 4

> `DatabaseMigrations` は，テストメソッド開始毎に毎回全テーブルをドロップして作り直しを行う

正しいです。非常に重い処理ゆえに，基本的にこれが使われることは無いでしょう。

> `DatabaseTransactions` は，テストメソッド開始毎にトランザクションを開始し，テストメソッド終了時にロールバックを行う

正しいです。テストごとにトランザクションがロールバックされる，効率的な実行方法です。但しテストの都合によってトランザクションが1個追加されてしまうため，場合によっては `DatabaseTruncation` を使わなければならないシーンがあるでしょう。

> `RefreshDatabase` は，テスト全体で 1 回だけ，テーブルのドロップと作り直しを行う

正しいです。おそらく最も使用率の高いトレイトです。以下のような動きをします。

- 全体で 1 回だけ `DatabaseMigrations` 相当の処理を実行
- テストメソッド開始毎に `DatabaseTransactions` 相当の処理を実行

---

## 6問目: 問題

UUID について，<ins>間違った</ins>説明をしているものはどれ？

1. `Str::uuid()` はデフォルトでは UUID v4 を返す
2. `Str::orderedUuid()` は UUID 標準に準拠していない
3. `Str::uuid()` `Str::orderedUuid()` の生成ロジックを，それぞれ別々にカスタムすることができる
4. `Str::orderedUuid()` の生成スペースは 2059 年に枯渇する

---

## 6問目: 回答

#### 正解: 3

> `Str::uuid()` `Str::orderedUuid()` の生成ロジックを，それぞれ別々にカスタムすることができる

カスタムはできますが，残念ながら 1 つしか指定することはできません。

（[`debug_backtrace()`](https://www.php.net/manual/ja/function.debug-backtrace.php) で呼び出し元を識別すれば使い分けることはできるかも :thinking:）

```php
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 マイクロ秒精度という無駄遣いが行われているので，意外と早く枯渇します。

https://github.com/ramsey/uuid/issues/392

---

# 以上！