一个函数解决 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_multisortusort/uasort 效率要高,这里引入一个 Schwartzian Transform(施瓦茨变换)概念。

Schwartzian Transform(又称 ​Decorate-Sort-Undecorate 模式)是一种优化排序的技术,它是 Randal L. Schwartz 提出的,他是 Perl 社区的知名程序员和作家,通过 ​​“装饰-排序-去装饰”​​(Decorate-Sort-Undecorate)模式减少重复计算。它的基本步骤是:

  1. Decorate(装饰)​:生成一个临时数组,存储原始数据及其计算后的排序键(避免重复计算)。
  2. Sort(排序)​:基于预计算的键进行排序(通常使用快速排序)。
  3. 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 插件中已经整合了该函数,可以直接使用。


©我爱水煮鱼,本站推荐使用的主机:阿里云,国外主机建议使用BlueHost

本站长期承接 WordPress 优化建站业务,请联系微信:「chenduopapa」。