wpjam_sort():一个函数解决 WordPress 数组所有排序的难题
我们之前介绍了 PHP 终极最强大的排序函数:array_multisort(),其中最强大的一点就是它能够基于多个条件对数组进行排序,可以将其理解为对标数据库 ORDER BY
的 PHP 本地化排序实现。
继续看一下之前的例子,比如可能想先按类别再按价格对产品列表进行排序:
$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_column
函数提取数组的操作可以在函数中搞定呢?把这些都简单化,改造成只要指定排序的数组,以及参与排序的字段以及顺序即可。
除了上面的问题之外,array_multisort
还几个问题:
首先是排序不稳定,在 PHP 官网也有提示:
如果两个成员完全相同,那么它们将保持原来的顺序。 在 PHP 8.0.0 之前,它们在排序数组中的相对顺序是未定义的。
这个意思就是在 PHP 7 的环境中,如果参与排序的字段,如果值是相同,谁在前面,是不确定。其实对于绝大部分情况来说,如果值是相同,应该原来谁在前面,排序之后还在前面。
第二,array_multisort
传递 $arr
的参数是引用参数,成功时返回 true
, 或者在失败时返回 false
,如果按照函数式编程的思维,最好还是返回排好顺序的新数组。
最后如果是数字索引数组,那么排序之后的结果是数字键名会被重新索引,但是我们有时候需要保留原来的 key。
这三个问题,我们都应该解决,特别是排序不稳定的问题。
wpjam_sort
所以我们创建了 wpjam_sort
函数,完美解决这些问题:
function wpjam_sort($arr, $options){
// 将选项中的字段通过 array_column 提取成数组
foreach($options as $k => $v){
$args[] = $column = array_column($arr, $k);
$args[] = in_array($v, [SORT_ASC, SORT_DESC], true) ? $v : (strtolower($v) === 'asc' ? SORT_ASC : SORT_DESC);
$args[] = is_numeric(current($column)) ? SORT_NUMERIC : SORT_REGULAR;
}
// 解决 PHP 7 排序不稳定的问题,将原本数组的顺序加入排序
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 )
和 wpjam_sort
很相似,那么为什么我们还要创建 wpjam_sort
函数呢?首先 wp_list_sort
函数内部是使用 usort
/uasort
实现的,这两个函数也有同样不稳定的问题,PHP 官方文档提示:
如果两个成员完全相同,那么它们将保持原来的顺序。 在 PHP 8.0.0 之前,它们在排序数组中的相对顺序是未定义的。
另外更重要的原因是使用 array_multisort
进行排序要比 usort
/uasort
效率要高。
为什么呢?这里给大家介绍编程中的一个概念:Schwartzian Transform(施瓦茨变换)。
Schwartzian Transform(又称 Decorate-Sort-Undecorate 模式)是一种优化排序的技术,它是 Perl 社区的知名程序员和作家 Randal L. Schwartz 提出的,通过 “装饰-排序-去装饰”(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
函数,使其也支持回调函数的方式进行排序。但是底层还是继续使用 array_multisort
进行排序,我们通过回调函数获取每个元素的值,然后降序排序:
function wpjam_sort($arr, ...$args){
// 空或者只有一个元素,就不用排序
if(count($arr) <= 1){
return $arr;
}
if(is_callable($args[0])){
// 通过回调函数对每个元素求值,组成数组,并且降序
$args = [array_map($args[0], $arr), SORT_DESC, SORT_NUMERIC];
}elseif(wpjam_is_assoc_array($args[0])){
$fields = $args[0];
$args = [];
// 将选项中的字段通过 array_column 提取成数组
foreach($fields as $k => $v){
$args[] = $column = array_column($arr, $k);
$args[] = in_array($v, [SORT_ASC, SORT_DESC], true) ? $v : (strtolower($v) === 'asc' ? SORT_ASC : SORT_DESC);
$args[] = is_numeric(current($column)) ? SORT_NUMERIC : SORT_REGULAR;
}
}
// 解决 PHP 7 排序不稳定的问题,将原本数组的顺序加入排序
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($items, fn($v)=> ($v['order'] ?? 10) - ($position ? ($v['position'] ?? 10)*1000 : 0));
因为 wpjam_sort
回调方式是按照回调函数结果降序排的,这里是通过负数的方式就把 position
改成升序,并且通过乘以 1000 来提高优先级。
最后再说明一下为什么 wpjam_sort
的回调函数模式效率也很高,因为它首先通过回调函数取得每个元素的值,生成排序所需的临时数组,再使用 array_multisort
进行排序,也是符合 Schwartzian Transform(施瓦茨变换)的 “装饰-排序-去装饰” 规则,所以也很高效。
总结
wpjam_sort
通过规则方式和回调函数两种方式实现了一个函数就解决 WordPress 数组排序问题,用的好,在 WordPress 中,可以解决所有排序的问题。😁
WPJAM Basic 插件中已经整合了该函数,可以直接使用。😎