一个函数解决 WordPress 数组所有排序的难题
前面我们介绍了 PHP 终极最强大的排序工具:array_multisort(),其中最强大的一点就是基于多个条件对数组进行排序,基本可以将其理解为 PHP 中对标数据库 ORDER BY
的本地化排序实现。
继续看一下之前的例子,比如可能想先按类别再按价格对产品列表进行排序:
$products = array(
array("name" => "Product A", "category" => "Electronics", "price" => 200),
array("name" => "Product B", "category" => "Clothing", "price" => 75),
array("name" => "Product C", "category" => "Electronics", "price" => 150),
array("name" => "Product D", "category" => "Clothing", "price" => 100)
);
// 先按类别升序排序,再按价格升序排序
array_multisort(array_column($products, 'category'), SORT_ASC, array_column($products, 'price'), SORT_ASC, $products);
print_r($products);
最终结果:
Array
(
[0] => Array
(
[name] => Product B
[category] => Clothing
[price] => 75
)
[1] => Array
(
[name] => Product D
[category] => Clothing
[price] => 100
)
[2] => Array
(
[name] => Product C
[category] => Electronics
[price] => 150
)
[3] => Array
(
[name] => Product A
[category] => Electronics
[price] => 200
)
)
array_multisort
的问题
从上例子中,我们如果对二维数组基于多个条件对数组进行排序,都要对每个参与排序的字段,先通过 array_column
函数提前一个数组,然后再指定排序顺序。
每次都要这样操作,这样略微稍显复杂,并且 array_multisort
函数的参数太复杂,每次使用都要查文档,我们是否可以有简单的使用方法和参数,多步操作可以在函数中搞定呢?而我们需要做的就是指定排序的数组,以及参与排序的字段以及顺序。
此外 array_multisort
还几个问题,首先是排序不稳定的问题,在 PHP 官网也有提示:
如果两个成员完全相同,那么它们将保持原来的顺序。 在 PHP 8.0.0 之前,它们在排序数组中的相对顺序是未定义的。
就是在 PHP 7 的环境中,如果参与排序的字段,如果值是相同,谁在前面,是不确定,哈哈,程序就是那么神奇,还可以不确定,😅 一般来说应该原来哪个元素原来在前面,就在前面。
那么这些问题,我们都应该解决,不然莫名其妙的问题出现的时候,头都大:
此外,array_multisort
传递 $arr
的参数是引用参数,成功时返回 true
, 或者在失败时返回 false
,按照函数式编程,最好返回数组 $arr
。
最后如果是数字索引数组,那么排序之后的结果是数字键名会被重新索引,但是我们有时候需要保持。
wpjam_sort
所以我们创建了 wpjam_sort 函数,完美解决这些问题:
function wpjam_sort($arr, $options){
// 将选项中的字段通过 array_column 提取成数组
foreach($options as $k => $v){
$config = is_array($v) ? $v : ['order'=>$v];
$order = $config['order'] ?? 'desc';
$args[] = $column = array_column($arr, $k);
$args[] = in_array($order, [SORT_ASC, SORT_DESC], true) ? $order : (strtolower($order) === 'asc' ? SORT_ASC : SORT_DESC);
$args[] = $config['flag'] ?? (is_numeric(current($column)) ? SORT_NUMERIC : SORT_REGULAR);
}
// 解决 PHP 7 排序不稳定的问题,将原本数组的顺序加入排序
if(version_compare(PHP_VERSION, '8.0.0') < 0){
array_push($args, range(1, count($arr)), SORT_ASC, SORT_NUMERIC);
}
// 如果是数字索引数组,将原来的 key 加入排序
if(wp_is_numeric_array($arr)){
$keys = array_keys($arr);
$args[] = &$keys;
}
$args[] = &$arr;
array_multisort(...$args);
// 如果是数字索引数组,将原来的键名合并回数组
if(isset($keys)){
$arr = array_combine($keys, $arr);
}
return $arr;
}
这样原来的先按类别再按价格对产品列表进行排序的代码可以改成:
// 原来的代码:
array_multisort(array_column($products, 'category'), SORT_ASC, array_column($products, 'price'), SORT_ASC, $products);
print_r($products);
// 使用 wpjam_sort 代码:
$arr = wpjam_sort($products, ['category'=>'asc', 'price'=>'asc']);
这样非常简洁也好理解。
Schwartzian Transform(施瓦茨变换)
其实 WordPress 也内置了相同的函数 wp_list_sort
函数:
function wp_list_sort( $input_list, $orderby = array(), $order = 'ASC', $preserve_keys = false )
但是函数内部是使用 usort
/uasort
实现的,而这两个函数也有同样的问题:
如果两个成员完全相同,那么它们将保持原来的顺序。 在 PHP 8.0.0 之前,它们在排序数组中的相对顺序是未定义的。
另外创建 wpjam_sort
的另外一个问题,因为 array_multisort
比 usort
/uasort
效率要高,这里引入一个 Schwartzian Transform(施瓦茨变换)概念。
Schwartzian Transform(又称 Decorate-Sort-Undecorate 模式)是一种优化排序的技术,它是 Randal L. Schwartz 提出的,他是 Perl 社区的知名程序员和作家,通过 “装饰-排序-去装饰”(Decorate-Sort-Undecorate)模式减少重复计算。它的基本步骤是:
- Decorate(装饰):生成一个临时数组,存储原始数据及其计算后的排序键(避免重复计算)。
- Sort(排序):基于预计算的键进行排序(通常使用快速排序)。
- Undecorate(解装饰):提取原始数据,丢弃临时键。
比如我们之前先按类别再按价格对产品列表进行排序的代码,如果使用 uasort
进行处理的话:
uasort($products, function($a, $b) {
// 1. 先比较 category(升序)
$categoryCompare = strcmp($a['category'], $b['category']);
if ($categoryCompare !== 0) {
return $categoryCompare;
}
// 2. 如果 category 相同,再比较 price(升序)
return $a['price'] <=> $b['price'];
});
这样每次比较的的时候,都要去获取每个元素的的类别和价格,而使用 wpjam_sort
的话,天然支持Schwartzian Transform 转换,只需要在开始之前通过 array_column
提取成数组,显然效率更快。
如果简单通过提取字段的值进行排序还不够,wpjam_sort
也支持回调,通过回调函数获取每个元素的值,然后降序排序:
function wpjam_sort($arr, ...$args){
// 空或者只有一个元素,就不用排序
if(count($arr) <= 1){
return $arr;
}
if(is_callable($args[0])){
// 通过回调函数对每个元素求值,组成数组,并且降序
$args = [array_map($args[0], (isset($args[1]) && $args[1] == 'key' ? array_keys($arr) : $arr)), SORT_DESC, SORT_NUMERIC];
}elseif(wpjam_is_assoc_array($args[0])){
$fields = $args[0];
$args = [];
// 将选项中的字段通过 array_column 提取成数组
foreach($fields as $k => $v){
$config = is_array($v) ? $v : ['order'=>$v];
$order = $config['order'] ?? 'desc';
$args[] = $column = array_column($arr, $k);
$args[] = in_array($order, [SORT_ASC, SORT_DESC], true) ? $order : (strtolower($order) === 'asc' ? SORT_ASC : SORT_DESC);
$args[] = $config['flag'] ?? (is_numeric(current($column)) ? SORT_NUMERIC : SORT_REGULAR);
}
}
// 解决 PHP 7 排序不稳定的问题,将原本数组的顺序加入排序
if(version_compare(PHP_VERSION, '8.0.0') < 0){
array_push($args, range(1, count($arr)), SORT_ASC, SORT_NUMERIC);
}
// 如果是数字索引数组,将原来的 key 加入排序
if(wp_is_numeric_array($arr)){
$keys = array_keys($arr);
$args[] = &$keys;
}
$args[] = &$arr;
array_multisort(...$args);
if(isset($keys)){
$arr = array_combine($keys, $arr);
}
return $arr;
}
比如下面的代码就是通过 wpjam_sort
回调的方式排序,先按照 position
升序,然后再按 order
降序。因为 wpjam_sort
回调方式是按照回调函数结果降序排的,这里是通过负数将 position
改成升序,并且通过乘以 1000 提高优先级。
wpjam_sort($items, fn($v)=> ($v['order'] ?? 10) - ($position ? ($v['position'] ?? 10)*1000 : 0));
总结
一句话总结 wpjam_sort
通过规则方式和回调函数两种方式实现了一个函数就解决 WordPress 数组排序问题,用的好,在 WordPress 中,可以解决所有排序的问题。😁
WPJAM Basic 插件中已经整合了该函数,可以直接使用。