Заказ коллекции Doctrine на основе связанного объекта, когда невозможно использовать аннотацию @orderBy

Я хотел бы понять, как лучше всего заказать коллекцию Doctrine на основе связанного Entity. В этом случае нельзя использовать аннотацию @orderBy.

Я нашел 5 решений в Интернете.

1) Добавление метода к AbstractEntity (согласно Ian Belter qaru.site/info/75600/...)

/**
 * This method will change the order of elements within a Collection based on the given method.
 * It preserves array keys to avoid any direct access issues but will order the elements
 * within the array so that iteration will be done in the requested order.
 *
 * @param string $property
 * @param array  $calledMethods
 *
 * @return $this
 * @throws \InvalidArgumentException
 */
public function orderCollection($property, $calledMethods = array())
{
    /** @var Collection $collection */
    $collection = $this->$property;

    // If we have a PersistentCollection, make sure it is initialized, then unwrap it so we
    // can edit the underlying ArrayCollection without firing the changed method on the
    // PersistentCollection. We're only going in and changing the order of the underlying ArrayCollection.
    if ($collection instanceOf PersistentCollection) {
        /** @var PersistentCollection $collection */
        if (false === $collection->isInitialized()) {
            $collection->initialize();
        }
        $collection = $collection->unwrap();
    }

    if (!$collection instanceOf ArrayCollection) {
        throw new InvalidArgumentException('First argument of orderCollection must reference a PersistentCollection|ArrayCollection within $this.');
    }

    $uaSortFunction = function($first, $second) use ($calledMethods) {

        // Loop through $calledMethods until we find a orderable difference
        foreach ($calledMethods as $callMethod => $order) {

            // If no order was set, swap k => v values and set ASC as default.
            if (false == in_array($order, array('ASC', 'DESC')) ) {
                $callMethod = $order;
                $order = 'ASC';
            }

            if (true == is_string($first->$callMethod())) {

                // String Compare
                $result = strcasecmp($first->$callMethod(), $second->$callMethod());

            } else {

                // Numeric Compare
                $difference = ($first->$callMethod() - $second->$callMethod());
                // This will convert non-zero $results to 1 or -1 or zero values to 0
                // i.e. -22/22 = -1; 0.4/0.4 = 1;
                $result = (0 != $difference) ? $difference / abs($difference): 0;
            }

            // 'Reverse' result if DESC given
            if ('DESC' == $order) {
                $result *= -1;
            }

            // If we have a result, return it, else continue looping
            if (0 !== (int) $result) {
                return (int) $result;
            }
        }

        // No result, return 0
        return 0;
    };

    // Get the values for the ArrayCollection and sort it using the function
    $values = $collection->getValues();
    uasort($values, $uaSortFunction);

    // Clear the current collection values and reintroduce in new order.
    $collection->clear();
    foreach ($values as $key => $item) {
        $collection->set($key, $item);
    }

    return $this;
}

2) Создание расширения Twig, если вам нужна сортировка только в шаблоне (согласно Kris qaru.site/info/75603/...)

use Doctrine\Common\Collections\Collection;

public function sort(Collection $objects, $name, $property = null)
{
    $values = $objects->getValues();
    usort($values, function ($a, $b) use ($name, $property) {
        $name = 'get' . $name;
        if ($property) {
            $property = 'get' . $property;
            return strcasecmp($a->$name()->$property(), $b->$name()->$property());
        } else {
            return strcasecmp($a->$name(), $b->$name());
        }
    });
    return $values;
}

3) Преобразование коллекции в массив, а затем ее сортировка (согласно Бенджамину Эберлею https://groups.google.com/d/msg/doctrine-user/zCKG98dPiDY/oOSZBMabebwJ)

public function getSortedByFoo()
{
    $arr = $this->arrayCollection->toArray();
    usort($arr, function($a, $b) {
    if ($a->getFoo() > $b->getFoo()) {
        return -1;
    }
    //...
    });
    return $arr;
}

4) Использование ArrayIterator для сортировки коллекции (согласно nifr qaru.site/info/75605/...)

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

5) Создание службы для сбора упорядоченной коллекции, а затем заменить неупорядоченный (у меня нет примера, но я думаю, что это довольно ясно). Я думаю, что это самое уродливое решение.

Какое наилучшее решение по вашему опыту? У вас есть другие предложения по заказу коллекции более эффективным/элегантным способом?

Большое спасибо.

Ответ 1

Предпосылка

Вы предложили 5 правильных/достойных решений, но я думаю, что все можно было сократить до двух случаев с некоторыми незначительными вариантами.

Мы знаем, что сортировка всегда O(NlogN), поэтому все решения теоретически имеют одинаковую производительность. Но так как это Доктрина, узкие места - это число SQL-запросов и методов гидратации (т.е. Преобразование данных из массива в экземпляр объекта).

Итак, вам нужно выбрать "лучший метод", в зависимости от того, когда вам нужны загружаемые объекты и что вы будете с ними делать.

Это мои "лучшие решения", и в общем случае я предпочитаю свое решение A)

A) DQL в службе загрузчика/хранилища

Аналогично

Ничего из вашего дела (как-то с 5, см. окончательную заметку). Альберто Фернандес указал вам в правильном направлении в комментарии.

Лучшее, когда

DQL (потенциально) самый быстрый метод, поскольку сортировка делегатов в СУБД, которая для этого оптимизирована. DQL также предоставляет полный контроль над тем, какие объекты будут извлекаться в одном запросе и режиме гидратации.

Недостатки

Невозможно (AFAIK) изменить запрос, созданный классами Proctrine Proxy, по конфигурации, поэтому вашему приложению необходимо использовать репозиторий и вызывать соответствующий метод каждый раз, когда вы загружаете объекты (или переопределяете их по умолчанию).

Пример

class MainEntityRepository extends EntityRepository
{
    public function findSorted(array $conditions)
    {
        $qb = $this->createQueryBuilder('e')
            ->innerJoin('e.association', 'a')
            ->orderBy('a.value')
        ;
        // if you always/frequently read 'a' entities uncomment this to load EAGER-ly
        // $qb->select('e', 'a');

        // If you just need data for display (e.g. in Twig only)
        // return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);

        return $qb->getQuery()->getResult();
    }
}

B) Желаемая загрузка и сортировка в PHP

Аналогично случаю

Случай 2), 3) и 4) - это то же самое, что и в другом месте. Моя версия - это общий случай, который применяется всякий раз, когда объекты извлекаются. Если вам нужно выбрать один из них, то я думаю, что решение 3) является наиболее удобным, так как не возиться с сущностью и всегда доступно, но используйте загрузку EAGER (читайте дальше).

Лучшее, когда

Если ассоциированные объекты всегда читаются, но невозможно (или удобно) добавлять службу, тогда все объекты должны загружаться EAGER-ly. Сортировка тогда может быть выполнена PHP, всякий раз, когда это имеет смысл для приложения: в прослушивателе событий, в контроллере, в шаблоне ветки... Если сущности всегда должны быть загружены, то наилучшим вариантом является прослушиватель событий.

Недостатки

Менее гибкий, чем DQL, и сортировка в PHP может быть медленной операцией, когда коллекция является большой. Кроме того, объекты должны быть гидратированы как объекты, которые медленны, и переполняют, если коллекция не используется для других целей. Остерегайтесь ленивой загрузки, так как это вызовет один запрос для каждого объекта.

Пример

MainEntity.orm.xml:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping>
  <entity name="MainEntity">
    <id name="id" type="integer" />
    <one-to-many field="collection" target-entity="LinkedEntity" fetch="EAGER" />
    <entity-listeners>
      <entity-listener class="MainEntityListener"/>
    </entity-listeners>
  </entity>
</doctrine-mapping>

MainEntity.php:

class MainEntityListener
{
    private $id;

    private $collection;

    public function __construct()
    {
        $this->collection = new ArrayCollection();
    }

    // this works only with Doctrine 2.5+, in previous version association where not loaded on event
    public function postLoad(array $conditions)
    {
        /*
         * From your example 1)
         * Remember that $this->collection is an ArryCollection when constructor is called,
         * but a PersistentCollection when are loaded from DB. Don't recreate the instance!
         */

        // Get the values for the ArrayCollection and sort it using the function
        $values = $this->collection->getValues();

        // sort as you like
        asort($values);

        // Clear the current collection values and reintroduce in new order.
        $collection->clear();
        foreach ($values as $key => $item) {
            $collection->set($key, $item);
        }
    }
}

Заключительные замечания

  • Я не буду использовать случай 1) как есть, так как это очень сложно и ввести наследование, которое уменьшает инкапсуляцию. Кроме того, я думаю, что он имеет такую ​​же сложность и производительность моего примера.
  • Случай 5) не обязательно плохой. Если "служба" - это репозиторий приложений, и она использует DQL для сортировки, то это мой первый лучший случай. Если это настраиваемая услуга только для сортировки коллекции, то я думаю, что это не очень хорошее решение.
  • Все коды, которые я написал здесь, не готовы для "copy-paste", поскольку моя цель состояла в том, чтобы показать мою точку зрения. Надеюсь, что это будет хорошей отправной точкой.

Отказ

Это "мои" лучшие решения, как я это делаю в своих работах. Надежда поможет вам и другим.