📝

PHPのアロー関数はLaravelのクエリビルダで活躍する

2023/02/23に公開

はじめに

皆さんPHP7.4から導入されたアロー関数は使っていますか?
最初はイマイチだったんですが使い所が見つかってきたので共有として記事を書きました

こんな感じのやつです

$users = User::all();
$activeUsers = $users->reject(fn ($user) => $user->is_deleted);

なんかJavaScriptっぽいな!
本記事ではこのアロー関数のユースケースについて書いていきます

アロー関数とは

アロー関数とは、無名関数を短く記述するための構文の一つです

  • function を fn に省略する
  • use は書かなくてもスコープ外の変数を使える
  • return は省略する(書けない)
  • 波括弧 {} は書けない
  • 1行しか書けない

ただJavaScriptとは異なり波括弧が書けず、1行しか書けないというのがどうも使いづらく、波括弧で複数行書けてもいいじゃん!
って思うんですが、、、残念ながら現状は1行しか書けません

これに関しては波括弧を使えるようにするプルリクがきていた模様
https://zenn.dev/rana_kualu/articles/d03f1261ca879a32e6c2

残念ながらRFCでは賛成27、反対16で僅差で却下になってしまったようです
惜しい、、、この機能欲しかった、、
https://wiki.php.net/rfc/auto-capture-closure

さて、このような制約はありますがアロー関数が活躍する場面があります

アロー関数の使い方

例えば、オブジェクトの配列から特定のプロパティを抽出する例を見てみましょう
この例では25歳以上のユーザーを抽出しています

$users = [
    (object)['name' => 'Alice', 'age' => 20],
    (object)['name' => 'Bob', 'age' => 25],
    (object)['name' => 'Charlie', 'age' => 30]
];
$minAge = 25;

// 普通に書いた例
$normal = array_filter($users, function ($user) use ($minAge) {
    return $user->age >= $minAge;
});

// アロー関数を使った例 $minAgeをuseしなくても自動でキャプチャされる
$useArrow = array_filter($users, fn ($user) => $user->age >= $minAge);

アロー関数を使用することで、無名関数をより短く簡潔に記述が出来ました

ただ現実的にはどうにも使いづらく、例えばarray_mapのような関数は複数行記述することが多く
残念ながらアロー関数は1行しか書けない制約があるため使用出来ないケースが多いです

しかし大活躍するケースがあります

Laravelのクエリビルダ

以下にクエリビルダでの活用例を掲載します

Laravelのクエリビルダでの活用

クエリビルダでいつ使うの?って話になると思うので特に私がよくアロー関数を使うメソッドです

  • orWhere
  • whereHas
  • join
  • when
  • unless

Laravelのアロー関数を使って、実際の実務でリファクタリングしたコード例を紹介します!
(元のソース乗せるわけにはいかないので原型が残らないレベルに改変しています)
この例では、店舗払いと銀行振込をチェックボックスで絞り込んだ検索について説明しています

クエリに条件を付与するコードがクロージャで複雑化していたのでアロー関数に置き換えました
まずは修正前のコードを見てください

// 修正前のコード
public function scopeSearchPayment($query, $bankTransfer, $shopPayment)
{
    if ($bankTransfer && $shopPayment) {
        return $query->where(function($q) {
            $q->where('is_bank', self::METHOD_BANK_ACTIVE)
                ->where(function($q) {
                    $q->whereHas('shopInfo', function($q) {
                        $q->where('bank_type', ShopInfo::TYPE_OFFICE);
                    })
                    ->orWhereHas('shopTable', function($q) {
                        $q->whereNull('ban_date');
                    });
                });
        })
        ->where(function($q) {
            $q->where('is_local', self::METHOD_LOCAL_ACTIVE)
                ->whereHas('shopInfo', function($q) {
                    $q->where('is_shop_payment', ShopInfo::TYPE_SHOP);
                })
                ->whereHas('shopTable', function($q) {
                    $q->whereNull('ban_date');
                });
        });
    } elseif ($bankTransfer) {
        return $query->where('is_bank', self::METHOD_BANK_ACTIVE)
            ->where(function($q) {
                $q->whereHas('shopInfo', function($q) {
                        $q->where('bank_type', ShopInfo::TYPE_OFFICE);
                    })
                    ->orWhereHas('shopTable', function($q) {
                        $q->whereNull('ban_date');
                    });
            });
    } elseif ($shopPayment) {
        return $query->where('is_local', self::METHOD_LOCAL_ACTIVE)
            ->whereHas('shopInfo', function($q) {
                $q->where('is_shop_payment', ShopInfo::TYPE_SHOP);
            })
            ->whereHas('shopTable', function($q) {
                $q->whereNull('ban_date');
            });
    }
    return $query;
}

これ…読めますか?w
私は途中でアレルギー反応が出そうになります笑
実際のコードはこれより更にぐちゃぐちゃでしたが、もはやネストしすぎてなにがどうなってるか読むのが大変です
(実際は大した条件分岐ではないですが…)

まずは先に結論として修正した後のコードを掲載します

// アロー関数使用バージョン
public function scopeSearchPayment(Builder $query, bool $bankTransfer, bool $shopPayment): Builder
{
    $bankTransferCondition = fn (Builder $q) => $q
        ->where('is_bank', self::METHOD_BANK_ACTIVE)
        ->where(fn (Builder $q) =>
            $q->whereHas('shopInfo', fn (Builder $q) => $q->where('bank_type', ShopInfo::TYPE_OFFICE))
            ->orWhereHas('shopTable', fn (Builder $q) => $q->whereNull('ban_date'))
        );

    $shopPaymentCondition = fn (Builder $q) => $q
        ->where('is_local', self::METHOD_LOCAL_ACTIVE)
        ->whereHas('shopInfo', fn (Builder $q) => $q->where('is_shop_payment', ShopInfo::TYPE_SHOP))
        ->whereHas('shopTable', fn (Builder $q) => $q->whereNull('ban_date'));

    return $query
        ->when($bankTransfer && $shopPayment, fn (Builder $q) => $q->where($bankTransferCondition)->where($shopPaymentCondition))
        ->when($bankTransfer && !$shopPayment, fn (Builder $q) => $q->where($bankTransferCondition))
        ->when(!$bankTransfer && $shopPayment, fn (Builder $q) => $q->where($shopPaymentCondition));
}

どうですか?これなら簡単ですよね!
このようにLaravelのクエリビルダはメソッドチェーンで書く場合が多いので
改行をしても1行のコードという事になるため、アロー関数が利用可能です

修正点は以下になります

  • 通常のクロージャで書いていた箇所をアロー関数に置き換え
  • 重複しているコードをまとめた

注意点

アロー関数は便利な反面注意すべき点があります
なんでもかんでもアロー関数で書けば良いわけではないということで

use文を使わずに、外部の変数をキャプチャする機能がある以上
変数のスコープ範囲には気をつける必要があります
またかえって読みにくいコードになってしまいがちな場合は大人しく省略せずに
書くのも良いかと思います!

以上となります!

Discussion