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;
});
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つのパラメーターを受け取るようになっています。
-
第一引数 (
$value
): これはコレクション内の現在のアイテムです。コールバック関数はこのアイテムに対して何らかの操作(例えば、特定の属性の値を取得する)を行い、その結果を返します。この結果は後にソートの基準として使用されます。 -
第二引数 (
$key
): これは現在のアイテムのキーです。このパラメーターはオプションであり、コールバック関数内で使用されない場合もあります。しかし、アイテムのキーに基づいて何らかの操作を行いたい場合には、このパラメーターが役立ちます。
例えば、あなたが示したコードスニペットでは、コールバック関数は各アイテムのid
、name_kana
、number
の属性を取得し、それらをアンダースコアで連結した文字列を返しています。これにより、得られた文字列はソートのための基準として利用されます。
$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
メソッドの最終段階であり、コレクションのアイテムをユーザー定義のソート条件に基づいて並び替えた後の処理を担当しています。具体的な処理の流れは以下の通りです。
-
ソート処理: 最初に、
$descending
の真偽値に基づいて、$results
配列を昇順(asort
)または降順(arsort
)でソートします。この$results
配列には、先に$callback
関数を適用して得られた各アイテムのソート基準が格納されています。このステップでのソートは、これらの基準を用いて行われます。 -
アイテムの再割り当て:
- この処理ブロックでは、ソートされた
$results
配列のキーを使用して、元のコレクション($this->items
)内のアイテムをソートされた順序に従って再配置しています。 -
foreach (array_keys($results) as $key)
によって、ソートされた配列$results
のすべてのキーを取得し、それらをループしています。このループの中で、各キーに対応する元のコレクションのアイテム($this->items[$key]
)を取得し、それを$results[$key]
に再割り当てしています。 - このステップの目的は、ソートに使用された基準(
$results
配列)に従って、元のコレクションのアイテムを正しい順序で$results
配列に格納し直すことです。
- この処理ブロックでは、ソートされた
-
コレクションのインスタンス返却:
- 最後に、ソートされたアイテムを含む
$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
クラスを使用した例は以下の通りです。
$collator = new Collator('ja_JP');
$array = ['あいう', 'かきく', 'さしす'];
$collator->asort($array);
print_r($array);
この方法では、Collator
オブジェクトが日本語のソートルールに従って配列をソートします。これにより、日本語の文字列を期待通りにソートすることが可能になります。
まとめると、PHPの標準のソート関数(asort
など)を使用した場合、日本語を含むマルチバイト文字列のソートはバイト値に基づくため、期待したソート結果にならないことがあります。日本語文字列のソートには、Collator
クラスやusort
関数とマルチバイト文字列関数を適切に組み合わせる方法が推奨されます。