71

In various cases I need to sort a Doctrine\Common\Collections\ArrayCollection according to a property in the object. Without finding a method doing that right away, I do this:

// $collection instanceof Doctrine\Common\Collections\ArrayCollection
$array = $collection->getValues();
usort($array, function($a, $b){
    return ($a->getProperty() < $b->getProperty()) ? -1 : 1 ;
});

$collection->clear();
foreach ($array as $item) {
    $collection->add($item);
}

I presume this is not the best way when you have to copy everything to native PHP array and back. I wonder if there is a better way to "usort" a Doctrine\Common\Collections\ArrayCollection. Do I miss any doc?

Nicolai Fröhlich
  • 51,330
  • 11
  • 126
  • 130
luiges90
  • 4,493
  • 2
  • 28
  • 43

6 Answers6

127

To sort an existing Collection you are looking for the ArrayCollection::getIterator() method which returns an ArrayIterator. example:

$iterator = $collection->getIterator();
$iterator->uasort(function ($a, $b) {
    return ($a->getPropery() < $b->getProperty()) ? -1 : 1;
});
$collection = new ArrayCollection(iterator_to_array($iterator));

The easiest way would be letting the query in the repository handle your sorting.

Imagine you have a SuperEntity with a ManyToMany relationship with Category entities.

Then for instance creating a repository method like this:

// Vendor/YourBundle/Entity/SuperEntityRepository.php

public function findByCategoryAndOrderByName($category)
{
    return $this->createQueryBuilder('e')
        ->where('e.category = :category')
        ->setParameter('category', $category)
        ->orderBy('e.name', 'ASC')
        ->getQuery()
        ->getResult()
    ;
}

... makes sorting pretty easy.

starball
  • 20,030
  • 7
  • 43
  • 238
Nicolai Fröhlich
  • 51,330
  • 11
  • 126
  • 130
  • Iterator does sort but doesn't seem to write through to the collection... Do I miss anything? – luiges90 May 23 '13 at 09:03
  • updated my answer! think i know what you wanted - added iterator_to_array ... acceptable now? :) – Nicolai Fröhlich May 23 '13 at 09:42
  • Can `uasort` by replaced with `usort`? I tried it but I get erorr -`Call to undefined method ArrayIterator::usort()` – Faery Aug 15 '13 at 09:08
  • usort hasn't been implemented for ArrayIterator ... so this is currently not possible. googlin' you will find several feature requests and some stackoverflow questions regarding this issue. hope that helps – Nicolai Fröhlich Aug 15 '13 at 14:54
  • 4
    For anyone else finding this you can also just convert the ArrayCollection to an array using $a = $collection->toArray(); followed by $collection = new ArrayCollection($a); – calumbrodie Apr 25 '14 at 11:00
  • @nifr How would you then use this new repo method in your entity? E.g. if I want `SuperEntity`'s `getCategory` method to return ordered results. – Sam Aug 14 '14 at 10:06
  • @Sam, I just got your issue... and finally found a workaround. For those interested, you can have a look to my question/answer [here](http://stackoverflow.com/q/27469222). – Alain Tiemblo Dec 14 '14 at 12:29
  • Answer http://stackoverflow.com/a/24246304/563394 below is now a much better option. – Ryan Apr 27 '16 at 23:41
  • @Ryan, but you can't use criteria if you're sorting by something that's not a property. Imagine an entity in the `ArrayCollection` has a property `tags` that is an `ArrayCollection` and you want to sort by the count of tags. I don't think criteria will help. – galeaspablo Aug 27 '16 at 16:03
  • That's true. Although the example the OP gives looks like a good candidate for it. The Doctrine Criteria implement is deliberately very minimal. – Ryan Aug 30 '16 at 00:00
  • This is worth noting, If you use the uasort, and your collection has numerical keys, make sure you set the second parameter of iterator_to_array as false and then assign it back to the collection, failure to do so may result in the array reverting back to the prior sort, This is especially true if you are returning the results in a json response. – ctatro85 May 07 '18 at 14:10
  • You need another `array_values` to get around with foreach. `$collection = new ArrayCollection(array_values(iterator_to_array($iterator)));` – Tamás András Horváth Oct 01 '19 at 09:11
66

Since Doctrine 2.3 you can use the Criteria API

Eg:

<?php

public function getSortedComments()
{
    $criteria = Criteria::create()
      ->orderBy(array("created_at" => Criteria::ASC));

    return $this->comments->matching($criteria);
}

Note: this solution requires public access to $createdAt property or a public getter method getCreatedAt().

ioleo
  • 4,697
  • 6
  • 48
  • 60
  • This will only work with Doctrine's ORM, the ODM's persistent collection does not support matching. – Logan Bailey Oct 21 '14 at 21:06
  • 2
    Actually you can do basic matching and sorting criteria on the collection objects themselves. From docs: "The Criteria has a limited matching language that works both on the SQL and on the PHP collection level.". Super convenient! – thinice Dec 12 '14 at 20:49
  • Doesn't seem to work if the property (ie. created_at) is not public :( – dubrox Feb 05 '15 at 16:33
  • I read or tested it and I believe it needs a public variable or a method named getCreated_At() or something like that to get the info. It sucks because I don't normally prepend get on my getter methods. – Clutch Apr 04 '16 at 20:29
  • @dubrox as stated by @Clutch, you need a public getter method `getCreatedAt` – ioleo Apr 20 '16 at 09:49
  • This is a much better solution now. – Ryan Apr 27 '16 at 23:40
  • downvoters: please explain why you downvoted? if this anwser could be improved, please share – ioleo Sep 12 '16 at 18:58
  • I had to call `array_values` on the return value of `matching()`, because it uses `uasort` which preserves the indexes. – Yep_It's_Me Mar 21 '18 at 07:52
31

If you have an ArrayCollection field you could order with annotations. eg:

Say an Entity named Society has many Licenses. You could use

/**
* @ORM\OneToMany(targetEntity="License", mappedBy="society")
* @ORM\OrderBy({"endDate" = "DESC"})
**/
private $licenses;

That will order the ArrayCollection by endDate (datetime field) in desc order.

See Doctrine documentation: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#orderby

Nepomuk Pajonk
  • 2,972
  • 1
  • 19
  • 29
akrz
  • 458
  • 5
  • 8
  • 6
    Be aware as it will not work with EAGER fetch mode. https://github.com/doctrine/doctrine2/issues/4256 – Aistis Oct 20 '16 at 11:49
2

Doctrine criteria does not allow to order by a property on a related object.

If you want to do it (like me), you have to use the uasort method of the Iterator like a previous response and if you use PHP 7, you can use the Spaceship operator <=> like this :

/** @var \ArrayIterator $iterator */
$iterator = $this->optionValues->getIterator();
$iterator->uasort(function (ProductOptionValue $a, ProductOptionValue $b) {
    return $a->getOption()->getPosition() <=> $b->getOption()->getPosition();
});

return new ArrayCollection(iterator_to_array($iterator));
Fabien Salles
  • 1,101
  • 15
  • 24
1

In last Symfony 5.3 without @annotations you just need

...
#[OrderBy(['sortOrder' => 'ASC'])]
private Collection $collection;

#[Column(type: 'integer')]
private int $sortOrder = 0;
...

in your entity

Mohamed Chaawa
  • 918
  • 1
  • 9
  • 23
  • `#[ORM\OrderBy(['sortOrder' => 'ASC'])]` most of time, because of `use Doctrine\ORM\Mapping as ORM;` – Erdal G. Jun 21 '22 at 16:33
0

How about getting all values, sorting these values, and then overwriting the property with the sorted values?

public function sortMyDateProperty(): void
{
    $values = $this->myAwesomeCollection->getValues();
    usort($values, static function (MyAwesomeInterface $a, MyAwesomeInterface $b): int {
        if ($a->getMyDateProperty()->getTimestamp() === $b->getMyDateProperty()->getTimestamp()) {
            return 0;
        }

        return $a->getMyDateProperty() > $b->getMyDateProperty() ? 1 : -1;
    });

    $this->myAwesomeCollection = new ArrayCollection($values);
}
Julian
  • 4,396
  • 5
  • 39
  • 51