web-dev-qa-db-fra.com

Quelle est la syntaxe pour trier une collection Eloquent sur plusieurs colonnes?

Je sais qu'en utilisant le générateur de requêtes, il est possible de trier par plusieurs colonnes en utilisant

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

mais maintenant j'ai affaire à un objet collection . Les collections ont la méthode sortBy, mais je n'ai pas pu comprendre comment la faire fonctionner pour plusieurs colonnes. Intuitivement, j'ai d'abord essayé d'utiliser la même syntaxe que orderBy.

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

mais cela n'applique apparemment que les tris de manière séquentielle et cela finit par être trié par colonne2, sans tenir compte de la colonne1. j'ai essayé

sortBy('column1', 'column2')

mais cela renvoie l'erreur "asort () s'attend à ce que le paramètre 2 soit long, chaîne donnée". En utilisant

sortBy('column1, column2')

ne renvoie pas d'erreur, mais le tri semble être assez aléatoire, donc je ne sais pas vraiment ce que cela fait réellement. J'ai regardé le code de la méthode sortBy , mais malheureusement, j'ai du mal à comprendre comment cela fonctionne.

41
Don't Panic

sortBy() prend une fermeture, vous permettant de fournir une valeur unique qui devrait être utilisée pour trier les comparaisons, mais vous pouvez en faire un composite en concaténant plusieurs propriétés ensemble

$posts = $posts->sortBy(function($post) {
    return sprintf('%-12s%s', $post->column1, $post->column2);
});

Si vous avez besoin du sortBy par rapport à plusieurs colonnes, vous aurez probablement besoin de les espacer pour vous assurer que "ABC" et "DEF" viennent après "AB" et "DEF", d'où le sprint à droite rembourré pour chaque colonne jusqu'à la longueur de la colonne ( au moins pour tous sauf la dernière colonne)

Notez qu'il est généralement beaucoup plus efficace si vous pouvez utiliser un orderBy dans votre requête afin que la collection soit prête à être triée lors de la récupération de la base de données

52
Mark Baker

J'ai trouvé une manière différente de le faire en utilisant sort() sur la collection éloquente. Cela peut potentiellement fonctionner un peu mieux ou au moins être un peu plus facile à comprendre que le remplissage des champs. Je serais intéressé de voir ce qui fonctionne mieux, car celui-ci a plus de comparaisons mais je ne fais pas la sprintf() pour chaque élément.

$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);
    }
);
37
derekaug

Comme @derekaug l'a mentionné, la méthode sort nous permet d'entrer une fermeture personnalisée pour trier la collection. Mais je pensais que sa solution était un peu lourde à écrire et ce serait bien d'avoir quelque chose comme ça:

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

En fait, cela peut être facilement archivé par l'encapsuleur $makeComparer Suivant pour générer la fermeture de comparaison:

$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;
};

Exemples

$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"],
* ];
*/

Maintenant, puisque nous parlons d'Eloquent ici, il y a de fortes chances que vous utilisiez également Laravel. Donc, nous pourrions même lier la fermeture de $makeComparer() à IOC et le résoudre à partir de là:

// 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;
                };
        });
    }
}

Vous pouvez désormais l'utiliser partout où vous le souhaitez:

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

Une solution simple consiste à enchaîner plusieurs fois sortBy () dans l'ordre inverse de la façon dont vous souhaitez les trier. L'inconvénient est que cela est probablement plus lent que le tri à la fois dans le même rappel, utilisez donc à vos risques et périls sur les grandes collections.

$collection->sortBy('column3')->sortBy('column2')->sortBy('column1');
1
dtbarne