如何使用语法按多列对Eloquent集合进行排序?

46

我知道在使用查询构建器时,可以使用多列进行排序

...orderBy('column1')->orderBy('column2')

但是现在我正在处理一个集合对象。 集合有sortBy方法,但我一直无法弄清楚如何使其适用于多个列。 直观地,我最初尝试使用与orderBy相同的语法。

sortBy('column1')->sortBy('column2)

但是这似乎只是按顺序应用排序,最终结果按第二列排序,而忽略了第一列。我尝试过。

sortBy('column1', 'column2')

但是这会抛出错误 "asort() expects parameter 2 to be long, string given"。使用

sortBy('column1, column2')

这不会抛出错误,但排序结果看起来相当随机,所以我真的不知道它实际上是做什么。我查看了sortBy方法的代码,但不幸的是,我很难理解它的工作原理。

4个回答

67

sortBy() 接受一个闭包,允许你提供一个单一的值作为排序比较依据,但你也可以通过将多个属性连接在一起来创建一个组合值。

$posts = $posts->sortBy(function($post) {
    return sprintf('%-12s%s', $post->column1, $post->column2);
});
如果您需要根据多列进行排序,可能需要使用空格填充它们,以确保"ABC"和"DEF"排在"AB"和"DEF"之后,因此需要为每一列进行右填充,直到达到该列的长度(至少对于除了最后一列之外的所有列)。
请注意,如果您可以在查询中使用orderBy,则通常更有效率,因此集合可以在从数据库检索时准备好排序。

3
如果您将属性名称作为字符串而不是闭包传递,我认为它会调用$this->valueRetriever($callback)来创建一个闭包来使用该属性名称......当然,该属性名称必须存在。 - Mark Baker
1
@Leith哪个更有效率?同时了解一下那个sprintf函数在做什么也是不错的。 - Jonathan
1
@Jonathan - 我不一定需要解释 已记录的PHP核心函数 做什么; 但在这种情况下,我已经解释了为什么要使用它(如果您需要对多个列进行sortBy,则可能需要对它们进行空格填充,以确保"ABC"和"DEF"在"AB"和"DEF"之后,因此对于每个列,至少向右填充到该列的长度(除了最后一列之外的所有列)) - Mark Baker
1
@Jonathan sprintf() 可能会略微更有效率(即如果你运行了10000次,你可能会看到几个微秒的差异)。我评论的原因是,虽然在某些情况下很有用,但我认为 sprintf() 是 C 语言的遗物 - 它的语法不直观,除非你非常熟悉它,否则甚至可以使简单的代码变得更难读。在这种情况下,作者使用它来将列值填充到12个字符,因此行为与我的略有不同。你需要进行“自然排序”才能避免这种“带空格的排序”黑客攻击。 - Leith
1
@YevgeniyAfanasyev 这完全取决于您想在回调中放置多少逻辑;我展示的简单回调只能用于简单的升序或降序;您可以使其更加复杂,但那是另一个问题... 尽管 derekaug 答案中的回调更适合按列强制进行升序/降序。 - Mark Baker
显示剩余4条评论

42

我发现了一种不同的方法,可以使用流畅集合中的sort()来完成。它可能比填充字段更好,或者至少更容易理解。这种方法有更多的比较,但我不会为每个项目执行sprintf()

$items = $items->sort(
    function ($a, $b) {
        // sort by column1 first, then 2, and so on
        return strcmp($a->column1, $b->column1)
            ?: strcmp($a->column2, $b->column2)
            ?: strcmp($a->column3, $b->column3);
    }
);

在Laravel 5.3上运行得非常好。谢谢! - Marcelo
1
即使使用5.5版本,也可以正常运行 - 如果您正在使用PHP 7并想要比较数字:我们现在有了新的太空船操作符! $a->id <=> $b->id - spaceemotion

13

正如@derekaug提到的,sort方法允许我们输入自定义闭包以对集合进行排序。但我认为他的解决方案写起来有点繁琐,所以有这样一种东西会很好:

$collection = collect([/* items */])
$sort = ["column1" => "asc", "column2" => "desc"];
$comparer = $makeComparer($sort);
$collection->sort($comparer);

实际上,可以通过以下$makeComparer包装器轻松地生成比较闭包:

$makeComparer = function($criteria) {
  $comparer = function ($first, $second) use ($criteria) {
    foreach ($criteria as $key => $orderType) {
      // normalize sort direction
      $orderType = strtolower($orderType);
      if ($first[$key] < $second[$key]) {
        return $orderType === "asc" ? -1 : 1;
      } else if ($first[$key] > $second[$key]) {
        return $orderType === "asc" ? 1 : -1;
      }
    }
    // all elements were equal
    return 0;
  };
  return $comparer;
};

示例

$collection = collect([
  ["id" => 1, "name" => "Pascal", "age" => "15"],
  ["id" => 5, "name" => "Mark", "age" => "25"],
  ["id" => 3, "name" => "Hugo", "age" => "55"],
  ["id" => 2, "name" => "Angus", "age" => "25"]
]);

$criteria = ["age" => "desc", "id" => "desc"];
$comparer = $makeComparer($criteria);
$sorted = $collection->sort($comparer);
$actual = $sorted->values()->toArray();

/**
* [
*  ["id" => 5, "name" => "Hugo", "age" => "55"],
*  ["id" => 3, "name" => "Mark", "age" => "25"],
*  ["id" => 2, "name" => "Angus", "age" => "25"],
*  ["id" => 1, "name" => "Pascal", "age" => "15"],
* ];
*/

$criteria = ["age" => "desc", "id" => "asc"];
$comparer = $makeComparer($criteria);
$sorted = $collection->sort($comparer);
$actual = $sorted->values()->toArray();

/**
* [
*  ["id" => 5, "name" => "Hugo", "age" => "55"],
*  ["id" => 2, "name" => "Angus", "age" => "25"],
*  ["id" => 3, "name" => "Mark", "age" => "25"],
*  ["id" => 1, "name" => "Pascal", "age" => "15"],
* ];
*/

$criteria = ["id" => "asc"];
$comparer = $makeComparer($criteria);
$sorted = $collection->sort($comparer);
$actual = $sorted->values()->toArray();

/**
* [
*  ["id" => 1, "name" => "Pascal", "age" => "15"],
*  ["id" => 2, "name" => "Angus", "age" => "25"],
*  ["id" => 3, "name" => "Mark", "age" => "25"],
*  ["id" => 5, "name" => "Hugo", "age" => "55"],
* ];
*/

现在,既然我们在谈论 Eloquent,那么你很有可能也正在使用 Laravel。因此,我们甚至可以将 $makeComparer() 闭包绑定到 IOC 并从那里解析它:

// app/Providers/AppServiceProvider.php 
// in Laravel 5.1
class AppServiceProvider extends ServiceProvider
{
    /**
     * ...
     */


    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind("collection.multiSort", function ($app, $criteria){
                return function ($first, $second) use ($criteria) {
                    foreach ($criteria as $key => $orderType) {
                        // normalize sort direction
                        $orderType = strtolower($orderType);
                        if ($first[$key] < $second[$key]) {
                            return $orderType === "asc" ? -1 : 1;
                        } else if ($first[$key] > $second[$key]) {
                            return $orderType === "asc" ? 1 : -1;
                        }
                    }
                    // all elements were equal
                    return 0;
                };
        });
    }
}
现在你可以在任何需要的地方使用它,如下所示:

现在您可以在需要的任何位置使用它,例如:

$criteria = ["id" => "asc"];
$comparer = $this->app->make("collection.multiSort",$criteria);
$sorted = $collection->sort($comparer);
$actual = $sorted->values()->toArray();

你可以使用 data_get 助手函数来获取“点符号”功能。非常有用: if (data_get($first, $key) < data_get($second, $key)) { return $orderType === "asc" ? -1 : 1; } else if (data_get($first, $key) > data_get($second, $key)) { return $orderType === "asc" ? 1 : -1; } - Rodolfo Jorge Nemer Nogueira

1
一个简单的解决方案是按照相反的顺序链式调用sortBy(),以达到你想要排序的效果。缺点是这种方法很可能比在同一个回调函数中一次性排序要慢,因此在处理大型集合时请自行决定是否使用。
$collection->sortBy('column3')->sortBy('column2')->sortBy('column1');

这看起来只有在sortBy使用稳定排序(http://wiki.c2.com/?StableSort)时才能正常工作。根据https://github.com/laravel/internals/issues/11上的讨论,这在Laravel 5.5中已经修复了。 - bdsl
这在 https://github.com/laravel/framework/pull/21255 中已被反转。截至2018年2月13日,仍没有“修复”。 - spaceemotion

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接