そもそもwhen()って何よ
公式ドキュメント Conditional Clausesによると、何らかの条件によってクエリに変化をつけたいときにこのように記述する、とある。
$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 () {}
とか書いちゃってる箇所は、どう考えても「無名関数」なので、改めて無名関数の説明を読む。
「例3 親のスコープからの変数の引き継ぎ」に注目。無名関数内の変数は、無名関数の外の変数と直接関係がないため、use()
を使って無名関数内で使いたい変数を引き継ぐ必要があるということのようだ。
クロージャは、変数を親のスコープから引き継ぐことができます。 引き継ぐ変数は、use で渡さなければなりません。 PHP 7.1 以降は、引き継ぐ値に superglobals や $this、およびパラメータと同じ名前の変数を含めてはいけません。
// $message を引き継ぎます
$example = function () use ($message) {
var_dump($message);
};
$example();
// 参照渡しで引き継ぎます
$example = function () use (&$message) {
var_dump($message);
};
$example();
ということは
冒頭のwhen()の箇所も、本当はこう書くんじゃないか?
$role = $request->input('role');
$users = DB::table('users')
->when($role, function ($query) use ($role) {
return $query->where('role_id', $role);
})
->get();
ということになり、わかっている人はuseを使っていたそうで。
無事解決しました。
追記2/3、動作確認環境
PHP 7.4.14
Laravel 7.29
追記2/3、GitHubのlaravel/docsにプルリクしました
追記2/4、Taylor Otwellさんにご指摘をいただきました
Laravel passes the first argument given to when to your callback.
とのご指摘をいただき、一晩考えました。
どこか勘違いがあるかもしれない。
そこで、実際に私が書いているコードを検証のために掲載してみます。
はじめに、うまく動かなかったコード:
->when($city !== '', function($q, $city) {
return $q->where("addr2", $city);
})
toSql()で得られるSQL文は形は正しいものの、バインドされた値が正しく伝わっていないのか、ほしい結果が得られませんでした。
次に、一旦動いたと思って採用したコード:
->when($city !== '', function($q) use($city) {
return $q->where("addr2", $city);
})
クロージャのuseを使って変数を渡すことでほしい結果が一応得られたのですが、Taylor Otwellさんの指摘を踏まえるともしかしてこうではないか?と見直しをしました:
->when($city, function($q, $city) {
if ($city !== '') {
return $q->where("addr2", $city);
}
else {
return $q;
}
})
要は、$city
が空だったら無駄なWHERE句を増やしたくないという意図なので、むしろこっちが正しいということになります。Taylor Otwellさんは間違ってない。
でも、今回はたまたま$city
一つの条件判定でした。
これがもしも複数個の変数をクロージャに渡す必要が生じてきたらどうなりますかね。
そのときこそ、クロージャのuse(...)
が必要になってきますよね。
->when(true, function ($q) use ($state, $city, $town, $section) { // こうなんじゃないか
if (...) {
return $q->where(...);
}
else {
return $q;
}
})
まぁ、実際そこまで複雑な判定をすることはないかもしれないですが、クロージャの内側に確実に変数の値を渡すにはuse (...)
が必要なんじゃないかなと言うのが私の主張です。
追記2/5、実装のソースコードをちゃんと見ればよかった
vendor/laravel/framework/src/Illuminate/Database/Concerns/BuilderQueries.php の中で、when()
は定義されています。
/**
* 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 への変換の話です。
もしも、$value
がゼロのときに$callback
を使いたいってとき、うっかり
->when($value, function ($query) { ... })
などと書いてしまうと、それ false やんってなりクロージャは呼ばれずじまいです。
(idなんかでゼロを使っちゃダメだろって話もありますけども、フラグとかゼロを避けずに設計しちゃううっかりさんがメンバーにいたりすると大変ですよね。ってのは余談ですが。)
で、ドキュメントに従ってこう書いてしまうと、これもおかしいわけです。
->when($value === 0, function ($query) {
$query->where('foo', $value);
})
これね、$query->where('foo', $value);
の$value
は、実装のソースコードで察すると$value === 0
の判定結果が入ります。$value
そのものじゃないんですね!
when()
の第一引数に条件式を書くなってことです。(←これ、私が最初に犯した失敗。)
(長くなったけど)when()
の第一引数にゼロ、'0'
、''
などBooleanに変換した際にfalseになってしまう値を使いたいときには、もう黙って true を書いてクロージャにuseを使うしかないってことです。
->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, ...)
なんですよ。
/**
* Pass the query to a given callback.
*
* @param callable $callback
* @return $this
*/
public function tap($callback)
{
return $this->when(true, $callback);
}
コードをスッキリ見せるためにも、tap()
使いましょうねと。
といったところで、長い蛇足にお付き合いいただきありがとうございました。