Laravelのクエリビルダでwhen()を使うときの書き方

### そもそもwhen()って何よ

[公式ドキュメント Conditional Clauses](https://laravel.com/docs/8.x/queries#conditional-clauses)によると、何らかの条件によってクエリに変化をつけたいときにこのように記述する、とある。

```php
$role = $request->input('role');

$users = DB::table('users')
                ->when($role, function ($query, $role) {
                    return $query->where('role_id', $role);
                })
                ->get();
```

それイイね、と思って真似してみたけど、どうもおかしい。

### PHPにおける「無名関数」(Anonymous functions, Closure)とは
`function () {}` とか書いちゃってる箇所は、どう考えても「無名関数」なので、改めて[無名関数](https://www.php.net/manual/ja/functions.anonymous.php#functions.anonymous)の説明を読む。
「例3 親のスコープからの変数の引き継ぎ」に注目。無名関数内の変数は、無名関数の外の変数と直接関係がないため、`use()`を使って無名関数内で使いたい変数を引き継ぐ必要があるということのようだ。

> クロージャは、変数を親のスコープから引き継ぐことができます。 引き継ぐ変数は、use で渡さなければなりません。 PHP 7.1 以降は、引き継ぐ値に superglobals や $this、およびパラメータと同じ名前の変数を含めてはいけません。

```php
// $message を引き継ぎます
$example = function () use ($message) {
    var_dump($message);
};
$example();
```

```php
// 参照渡しで引き継ぎます
$example = function () use (&$message) {
    var_dump($message);
};
$example();
```

### ということは

冒頭のwhen()の箇所も、本当はこう書くんじゃないか？

```php
$role = $request->input('role');

$users = DB::table('users')
                ->when($role, function ($query) use ($role) {
                    return $query->where('role_id', $role);
                })
                ->get();
```

ということになり、[わかっている人はuseを使っていた](https://twitter.com/kosukejlampnet/status/1356858433656381445)そうで。

無事解決しました。

### 追記2/3、動作確認環境
PHP 7.4.14
Laravel 7.29

### 追記2/3、GitHubのlaravel/docsにプルリクしました
[近々ドキュメントに反映されるようです。](https://github.com/laravel/docs/pull/6812#issuecomment-772392531)

### 追記2/4、Taylor Otwellさんに[ご指摘をいただきました](https://github.com/laravel/docs/pull/6811#issuecomment-772508416)

> Laravel passes the first argument given to when to your callback.

とのご指摘をいただき、一晩考えました。
どこか勘違いがあるかもしれない。
そこで、実際に私が書いているコードを検証のために掲載してみます。
はじめに、うまく動かなかったコード：

```php
->when($city !== '', function($q, $city) {
    return $q->where("addr2", $city);
})
```

toSql()で得られるSQL文は形は正しいものの、バインドされた値が正しく伝わっていないのか、ほしい結果が得られませんでした。
次に、一旦動いたと思って採用したコード：

```php
->when($city !== '', function($q) use($city) {
    return $q->where("addr2", $city);
})
```

クロージャのuseを使って変数を渡すことでほしい結果が一応得られたのですが、Taylor Otwellさんの指摘を踏まえるともしかしてこうではないか？と見直しをしました：

```php
->when($city, function($q, $city) {
    if ($city !== '') {
        return $q->where("addr2", $city);
    }
    else {
        return $q;
    }
})
```

要は、`$city`が空だったら無駄なWHERE句を増やしたくないという意図なので、むしろこっちが正しいということになります。Taylor Otwellさんは間違ってない。

でも、今回はたまたま`$city`一つの条件判定でした。
これがもしも複数個の変数をクロージャに渡す必要が生じてきたらどうなりますかね。
そのときこそ、クロージャの`use(...)`が必要になってきますよね。

```php
->when(true, function ($q) use ($state, $city, $town, $section) {  // こうなんじゃないか
    if (...) {
        return $q->where(...);
    }
    else {
        return $q;
    }
})
```

まぁ、実際そこまで複雑な判定をすることはないかもしれないですが、クロージャの内側に確実に変数の値を渡すには`use (...)`が必要なんじゃないかなと言うのが[私の主張](https://github.com/laravel/docs/pull/6811#issuecomment-772935829)です。

### 追記2/5、実装のソースコードをちゃんと見ればよかった
vendor/laravel/framework/src/Illuminate/Database/Concerns/BuilderQueries.php の中で、`when()`は定義されています。

```php
    /**
     * Apply the callback's query changes if the given "value" is true.
     *
     * @param  mixed  $value
     * @param  callable  $callback
     * @param  callable|null  $default
     * @return mixed|$this
     */
    public function when($value, $callback, $default = null)
    {
        if ($value) {
            return $callback($this, $value) ?: $this;
        } elseif ($default) {
            return $default($this, $value) ?: $this;
        }

        return $this;
    }
```

when()の第一引数`$value`が、そのまま`if ($value)`の判定に使われているところからして、思い出すのが[boolean への変換](https://www.php.net/manual/ja/language.types.boolean.php#language.types.boolean.casting)の話です。
もしも、`$value`がゼロのときに`$callback`を使いたいってとき、うっかり

```php
->when($value, function ($query) { ... })
```

などと書いてしまうと、それ false やんってなりクロージャは呼ばれずじまいです。
（idなんかでゼロを使っちゃダメだろって話もありますけども、フラグとかゼロを避けずに設計しちゃううっかりさんがメンバーにいたりすると大変ですよね。ってのは余談ですが。）
で、ドキュメントに従ってこう書いてしまうと、これもおかしいわけです。

```php
->when($value === 0, function ($query) {
  $query->where('foo', $value);
})
```

これね、`$query->where('foo', $value);`の`$value`は、実装のソースコードで察すると`$value === 0`の判定結果が入ります。**`$value`そのものじゃない**んですね！
**`when()`の第一引数に条件式を書くな**ってことです。（←これ、私が最初に犯した失敗。）
（長くなったけど）`when()`の第一引数にゼロ、`'0'`、`''`などBooleanに変換した際にfalseになってしまう値を使いたいときには、もう黙って true を書いてクロージャにuseを使うしかないってことです。

```php
->when(true, function ($query) use ($value) {  // これなら確実にクロージャを実行できる
  if ($value === 0) {  // 条件判定をクロージャの中で実行する
    $query->where('foo', $value);
  }
})
```

でも、そこは`when()`を使わずに、`tap()`ってのがあるんですね。
vendor/laravel/framework/src/Illuminate/Database/Concerns/BuilderQueries.php の `when()` に実装のすぐあとに記述されている `tap()` が、`when(true, ...)` なんですよ。

```php
    /**
     * Pass the query to a given callback.
     *
     * @param  callable  $callback
     * @return $this
     */
    public function tap($callback)
    {
        return $this->when(true, $callback);
    }
```

コードをスッキリ見せるためにも、`tap()`使いましょうねと。

といったところで、長い蛇足にお付き合いいただきありがとうございました。