laravelのCollection sortByで複数カラムでのソート

laravelのCollection sortByで複数カラムでのソートをする機会があったので記事に残しておきます。
まず、どのようにソートをしたいかと言うと。。。

以下のデータがあります。
>category_code  003, name_kana 'ウエオ', number 10005
category_code 002, name_kana 'イウエ' number 10003
category_code 002, name_kana 'イウエ' number 10004
category_code 001, name_kana 'アイウ' number 10002
category_code 001, name_kana 'アイウ' number 10001

category_code昇順、name_kana昇順、number昇順の順番でソートしたい
以下のように
>category_code 001, name_kana 'アイウ' number 10001
category_code 001, name_kana 'アイウ' number 10002
category_code 002, name_kana 'イウエ' number 10003
category_code 002, name_kana 'イウエ' number 10004
category_code 003, name_kana 'ウエオ', number 10005


以下のように書いたら複数条件が可能とissuesには書いていました。
```
$collection->sortBy(function($item) {
  return $item->category_code. '_' .  $item->name_kana . '_' . $item->number;
});
```
>引用 https://github.com/laravel/ideas/issues/11


## sortBy関数を追う

```
    /**
     * Sort the collection using the given callback.
     *
     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback
     * @param  int  $options
     * @param  bool  $descending
     * @return static
     */
    public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
    {
        if (is_array($callback) && ! is_callable($callback)) {
            return $this->sortByMany($callback);
        }

        $results = [];

        $callback = $this->valueRetriever($callback);

        // First we will loop through the items and get the comparator from a callback
        // function which we were given. Then, we will sort the returned values and
        // grab all the corresponding values for the sorted keys from this array.
        foreach ($this->items as $key => $value) {
            $results[$key] = $callback($value, $key);
        }

        $descending ? arsort($results, $options)
            : asort($results, $options);

        // Once we have sorted all of the keys in the array, we will loop through them
        // and grab the corresponding model so we can set the underlying items list
        // to the sorted version. Then we'll just return the collection instance.
        foreach (array_keys($results) as $key) {
            $results[$key] = $this->items[$key];
        }

        return new static($results);
    }
```
>引用 https://github.com/laravel/framework/blob/11.x/src/Illuminate/Collections/Collection.php#L1399

この行は、falseのため飛ばす
```
        if (is_array($callback) && ! is_callable($callback)) {
            return $this->sortByMany($callback);
        }
```

以下の箇所は何しているのか。
```
        $callback = $this->valueRetriever($callback);

        // First we will loop through the items and get the comparator from a callback
        // function which we were given. Then, we will sort the returned values and
        // grab all the corresponding values for the sorted keys from this array.
        foreach ($this->items as $key => $value) {
            $results[$key] = $callback($value, $key);
        }
```

`$callback = $this->valueRetriever($callback);`行は、与えられた`$callback`をラップまたは変更して、`sortBy`メソッド内で使用できる形に加工する役割を担っています。具体的には、`valueRetriever`メソッド（または関数）は、`$callback`が属性名の文字列かクロージャ（無名関数）かに関わらず、それを処理し、コレクション内の各アイテムに適用できる形式のコールバックを生成します。このプロセスにより、さまざまなタイプの`$callback`が`sortBy`メソッドで一貫して扱われるようになります。

次の`foreach`ループについてですが、この部分はコレクションの各アイテムに対して前述のコールバック関数を実行し、その結果を`$results`配列に格納しています。ここで`$callback($value, $key);`としている部分では、コールバック関数が2つのパラメーターを受け取るようになっています。

1. **第一引数 (`$value`)**: これはコレクション内の現在のアイテムです。コールバック関数はこのアイテムに対して何らかの操作（例えば、特定の属性の値を取得する）を行い、その結果を返します。この結果は後にソートの基準として使用されます。
2. **第二引数 (`$key`)**: これは現在のアイテムのキーです。このパラメーターはオプションであり、コールバック関数内で使用されない場合もあります。しかし、アイテムのキーに基づいて何らかの操作を行いたい場合には、このパラメーターが役立ちます。

例えば、あなたが示したコードスニペットでは、コールバック関数は各アイテムの`id`、`name_kana`、`number`の属性を取得し、それらをアンダースコアで連結した文字列を返しています。これにより、得られた文字列はソートのための基準として利用されます。

```php
$normalList->sortBy(function ($item) {
    return $item->category_code. '_' .  $item->name_kana . '_' . $item->number;
})
```

この処理全体の目的は、コレクション内のアイテムをユーザーが指定した基準に基づいてソートすることです。`$callback`を適用することで、各アイテムからソートキーを生成し、そのキーに従ってアイテムを並び替えます。

## $resultsをデバックすると

```
array(5)
0:"003_ウエオ_10005"
1:"002_イウエ_10003"
2:"002_イウエ_10004"
3:"001_アイウ_10002"
4:"001_アイウ_10001"
```

以下の箇所は何しているのか。
```
        $descending ? arsort($results, $options)
            : asort($results, $options);

        // Once we have sorted all of the keys in the array, we will loop through them
        // and grab the corresponding model so we can set the underlying items list
        // to the sorted version. Then we'll just return the collection instance.
        foreach (array_keys($results) as $key) {
            $results[$key] = $this->items[$key];
        }
```

このコードの部分は、`sortBy`メソッドの最終段階であり、コレクションのアイテムをユーザー定義のソート条件に基づいて並び替えた後の処理を担当しています。具体的な処理の流れは以下の通りです。

1. **ソート処理**: 最初に、`$descending`の真偽値に基づいて、`$results`配列を昇順（`asort`）または降順（`arsort`）でソートします。この`$results`配列には、先に`$callback`関数を適用して得られた各アイテムのソート基準が格納されています。このステップでのソートは、これらの基準を用いて行われます。

2. **アイテムの再割り当て**:
    - この処理ブロックでは、ソートされた`$results`配列のキーを使用して、元のコレクション（`$this->items`）内のアイテムをソートされた順序に従って再配置しています。
    - `foreach (array_keys($results) as $key)`によって、ソートされた配列`$results`のすべてのキーを取得し、それらをループしています。このループの中で、各キーに対応する元のコレクションのアイテム（`$this->items[$key]`）を取得し、それを`$results[$key]`に再割り当てしています。
    - このステップの目的は、ソートに使用された基準（`$results`配列）に従って、元のコレクションのアイテムを正しい順序で`$results`配列に格納し直すことです。

3. **コレクションのインスタンス返却**:
    - 最後に、ソートされたアイテムを含む`$results`配列を使用して、更新されたコレクションの新しいインスタンスを作成し、それを返却します。これにより、呼び出し元はソートされたコレクションを受け取ることができます。

このプロセスを通じて、`sortBy`メソッドはコレクション内のアイテムをユーザーが定義した基準に基づいてソートし、その結果としてソートされた新しいコレクションインスタンスを提供します。

## 再度、$resultsをデバックすると

```
$results
array(5)
4:"001_アイウ_10001"
3:"001_アイウ_10002"
1:"002_イウエ_10003"
2:"002_イウエ_10004"
0:"003_ウエオ_10005"
```

## asort 日本語はどのように処理されている？

PHPの`asort`関数による日本語（や他のUTF-8でエンコードされたマルチバイト文字）の処理は、基本的にバイト単位で行われます。これは、PHPの文字列比較がデフォルトでバイト値に基づいているためです。しかし、このアプローチにはいくつかの注意点があります。

### バイト単位の比較:
日本語を含むUTF-8文字列は、複数のバイトで1文字が構成されています。`asort`を使用すると、これらのバイトが個別に比較され、結果として得られるソート順序はバイト値に基づきます。これは、英数字やASCII文字に対しては直感的な結果をもたらすことが多いですが、日本語のようなマルチバイト文字に対しては直感的でない結果になることがあります。

### 日本語の処理:
日本語を適切にソートするためには、文化的、言語的なルールに基づくソートが必要です。例えば、辞書順（五十音順）や人名のソートなどです。`asort`関数はこれらのルールを直接サポートしていません。

### 解決策:
日本語文字列を正しくソートするには、PHPの`usort`関数と`mb_strlen`や`mb_strtolower`などのマルチバイト文字列関数を組み合わせる方法があります。または、`Collator`クラス（`intl`拡張機能が必要）を使用してロケールに基づくソートを実行する方法もあります。

`Collator`クラスを使用した例は以下の通りです。

```php
$collator = new Collator('ja_JP');
$array = ['あいう', 'かきく', 'さしす'];
$collator->asort($array);
print_r($array);
```

この方法では、`Collator`オブジェクトが日本語のソートルールに従って配列をソートします。これにより、日本語の文字列を期待通りにソートすることが可能になります。

まとめると、PHPの標準のソート関数（`asort`など）を使用した場合、日本語を含むマルチバイト文字列のソートはバイト値に基づくため、期待したソート結果にならないことがあります。日本語文字列のソートには、`Collator`クラスや`usort`関数とマルチバイト文字列関数を適切に組み合わせる方法が推奨されます。