前回に続き、Collectionのsortメソッドについて解説していきます。今回は複数の項目を使ったソートについてです。

前回同様、以下のサンプルデータを使いますのでコピペして手を動かしつつ読み進めていただけると幸いです。

// 再掲
$houses = collect([
    ['id' => 1, 'price' => 3000, 'size' => 100, 'age' => 20, 'station_distance' => 0.6],
    ['id' => 10, 'price' => 3000, 'size' => 70, 'age' => 3, 'station_distance' => 1.1],
    ['id' => 23, 'price' => 1600, 'size' => 60, 'age' => 13],
    ['id' => 3, 'price' => 1600, 'size' => 50, 'age' => 11, 'station_distance' => 2.0],
]);

複数の項目によるソート

前回は物件情報を例にsort, sortBy, sortKeysを使い、広さや築年数など1つの項目をピックアップして並び替える方法について解説しました。しかし、時としてある項目で並び替えた後に値が同じであるならば、別の項目によって並び替えたいケースがあります。

例えば、物件情報を価格の安い順に並び替えた後、同じ価格の物件が複数あるならそれらを築年数が浅い順に並び替えたい、などの場合です。このような複雑な並び替えは、sort()の引数に比較ロジックをcallback関数で指定します。

sort($callback)

Laravel公式のドキュメントにはsort()にcallback関数を渡す場合について、実例が載っていませんが、

If your sorting needs are more advanced, you may pass a callback to sort with your own algorithm. Refer to the PHP documentation on uasort, which is what the collection’s sort method calls under the hood.

と書かれています。ので、PHPのuasortのドキュメントを確認しましょう。そちらの例1 基本的な uasort() の例にてcallback関数の例が示されています。

<?php
// 比較用の関数
function cmp($a, $b) {
    if ($a == $b) {
        return 0;
    }
    return ($a < $b) ? -1 : 1;
}

なるほど、$aと$bを引数に取り大小を比較する関数ですね。$a < $b なら1、$a > $b なら−1、$a = $b なら0が返されます。

例を参考に、まずは価格(昇順)で並び替えるcallback関数を書いてみましょう。

>>> function cmpPrice($a, $b) {
        if ($a['price'] == $b['price']) {
            return 0;
        }
 
        return ($a['price'] < $b['price']) ? -1 : 1;
    }

そして、それをsort()に指定してみましょう。

>>> $houses->sort('cmpPrice');
=> Illuminate\Support\Collection {#3397
     all: [
       2 => [
         "id" => 23,
         "price" => 1600,
         "size" => 60,
         "age" => 13,
       ],
       3 => [
         "id" => 3,
         "price" => 1600,
         "size" => 50,
         "age" => 11,
         "station_distance" => 2.0,
       ],
       0 => [
         "id" => 1,
         "price" => 3000,
         "size" => 100,
         "age" => 20,
         "station_distance" => 0.6,
       ],
       1 => [
         "id" => 10,
         "price" => 3000,
         "size" => 70,
         "age" => 3,
         "station_distance" => 1.1,
       ],
     ],
   }

価格が安い順にソートされましたね。正しくソートされているか不安な場合は前回紹介したsortBy()を使って確認してみましょう。

>>> $houses->sort('cmpPrice') == $houses->sortBy('price');
=> true

因みに、cmpPriceは昇順にソートされますが、

return ($a['price'] < $b['price']) ? -1 : 1;

この部分を

return ($a['price'] < $b['price']) ? 1 : -1;

とプラスマイナスを逆にすれば、降順にする事もできます。

続いて、価格が同じだった場合に築年数で比較するロジックを追加してみましょう。
価格の比較では名前付き関数を用意しましたが、次のコードではsort()内で無名関数で指定しています。laravelではこちらが一般的です。コードは以下のようになります。

>>> $houses->sort(function ($a, $b) {
        if ($a['price'] == $b['price']) {
            if ($a['age'] == $b['age']) {
                return 0;
            }
    
            return ($a['age'] < $b['age']) ? -1 : 1;
        }
    
        return ($a['price'] < $b['price']) ? -1 : 1;
    });
=> Illuminate\Support\Collection {#3397
     all: [
       3 => [
         "id" => 3,
         "price" => 1600,
         "size" => 50,
         "age" => 11,
         "station_distance" => 2.0,
       ],
       2 => [
         "id" => 23,
         "price" => 1600,
         "size" => 60,
         "age" => 13,
       ],
       1 => [
         "id" => 10,
         "price" => 3000,
         "size" => 70,
         "age" => 3,
         "station_distance" => 1.1,
       ],
       0 => [
         "id" => 1,
         "price" => 3000,
         "size" => 100,
         "age" => 20,
         "station_distance" => 0.6,
       ],
     ],
   }

価格だけで並び替えた際と見比べてください、同じ価格の場合に築浅順に並び替えられていますね。

今回は価格と築年数のみを考慮しましたが、同様に比較ロジックを追加していけば更に細かく並び順を指定できます。しかし、上記のコードではifの入れ子が増え、とても読み辛いコードになってしまいます。でも安心して下さい、次に紹介する宇宙船演算子エルビス演算子の合せ技でとてもスッキリさせることができます!

宇宙船演算子(<=>)とエルビス演算子(?:)の合せ技

2つの演算子について軽くおさらいしておきましょう。

宇宙船演算子

宇宙演算子はPHP7から追加された比較演算子で以下のように記述します。

$a <=> $b;

$aと$bを比較し、$a > $bなら1、$a = $bなら0、 $a < $bなら-1を返します。

お気づきかと思いますが、まさに前項で作成したcallback関数と同じ結果を返してくれます。

それにしても面白い名前ですね。初めてこの演算子を見た時は、使い方よりもまず名前の由来を検索してしまいました。諸説色々あるみたいですが、中でもStar Warsのダースベーダー専用タイファイターに似ている!というのが有力なのだとか。。。気になる人はググってみて下さい!

エルビス演算子

もう1つキーとなるのがエルビス演算子です。こちらはPHP5.3からあるので馴染みのある演算子かと思います。

$a ?: $b;

$aがtrueなら$aを返し、falseなら$bを返します。

エルビス演算子の由来は、歌手のエルヴィス・プレスリーの顔文字だそうです。。。どこが?っとなりますが、確かにハテナマークがリーゼントに見えなくも無いかも・・・。私はよくNULL合体演算子と混同してしまうのですが、こちらも名前の由来を知ってから忘れなくなりました。

宇宙船演算子とエルビス演算子を組み合わせると前項のコードは以下のように書き換えられます。

>>> $houses->sort(function ($a, $b) {
        return $a['price'] <=> $b['price']
            ?: $a['age'] <=> $b['age'];
    });

とてもスッキリしましたね!価格が同じ場合は $a['price'] <=> $b['price']の結果が0となり、booleanに変換するとfalseなのでエルビス演算子により築年数の比較に進む、というわけです。

さらに築年数が同じなら広さ、広さが同じなら駅からの距離、と並び替えたい場合は以下のように2行加えるだけです。

 
>>> $houses->sort(function ($a, $b) {
        return $a['price'] <=> $b['price']
            ?: $a['age'] <=> $b['age']
            ?: ($a['size'] <=> $b['size']) * -1
            ?: ($a['station_distance'] <=> $b['station_distance']) * -1;
    }); 

簡単ですね!

sizeとstation_distanceは降順に並び替えたいので-1を掛けています、ご注意を。

Laravel8のsortBy()

ここまでLaravel7を前提に複数の項目で並び替える方法を紹介してきました。しかし実はこれ、Laravel8ではsortBy()でもっと簡単に同じことができます。

公式ドキュメントを見ると、sortBy()並べ替える属性と目的の並べ替えの方向で構成される配列が指定できる旨が書いてあります。よって、Laravel8では以下で同じことが出来ます。

$houses->sortBy([
    ['price', 'asc'],
    ['age', 'asc'],
    ['size', 'desc'],
    ['station_distance', 'desc'],
]);

こちらの方が分かりやすいですね、Laravel8にアップグレードした際はこちらを使っていきたいと思います。




By hikaru