Сортировка моделей и генерация uri

Возможно вы обратили внимание, что при создании категории в нашей прошлой заметке, она получалась с rank = 0, и выводилась вверху дерева.
Это потому, что у модели категории, как и у прочих моделей, не прописана логика выставления этого самого rank. И сейчас мы её пропишем.
А потом подумаем, как обновлять uri категорий и товаров при перемещении в дереве.

Сортировочный трейт

Согласно нашей схеме БД, прямо сейчас у нас есть колонка rank в следующих моделях:
  • Language
  • Category
  • Product
  • ProductCategory
  • ProductFile
  • ProductLink
В общем, много где. Хотелось бы как-то управлять этим централизованно, да?
Поэтому пишем новый трейт в Models/Traits/RankedModel.php:
<?php

namespace App\Models\Traits;

trait RankedModel
{
    public static function bootRankedModel(): void
    {
        static::creating(
            static function (self $record) {
                if (!$record->rank) {
                    $record->rank = $record->getCurrentRank();
                }
            }
        );
    }

    protected function getCurrentRank(): int
    {
        return $this->newQuery()->max('rank') + 1;
    }
}
Добавляем трейт в модели и теперь при создании новой записи, в rank будет проставляться такое значение, чтобы она оказалась в конце.
Это отлично работает для обычных моделей, но со вложенными категориями будут проблемы, потому что у них rank зависит от родителя.
В таком случае, мы просто перезаписываем метод их трейта собственным, в модели Category:
    protected function getCurrentRank(): int
    {
        $c = $this->newQuery();
        if ($this->parent_id) {
            $c->where('parent_id', $this->parent_id);
        } else {
            $c->whereNull('parent_id');
        }

        return $c->max('rank') + 1;
    }
Теперь значение зависит от родительской категории.
Примерно так же прописываем получение rank и у товаров:
    protected function getCurrentRank(): int
    {
        return $this->newQuery()->where('category_id', $this->category_id)->max('rank') + 1;
    }
Товаров без категории быть не может, так что здесь условие попроще.

Обновление uri

Мы уже прописали сохранение полного uri в моделях Category и Product, но это работает только при сохранении непосредственно модели, и никак не касается её связей.
А при перетаскивании категории в дереве нужно обновлять всё в неё вложенное. Как это лучше сделать?
Я предлагаю обновить booted функцию модели Category:
<?php

namespace App\Models;

// ...

class Category extends Model
{
    // ...

    // Индикатор необходимости обновления uri категории
    public static bool $updateUri = false;

    protected static function booted(): void
    {
        static::saving(static function (self $model) {
            $uri = [$model->alias];
            if ($model->parent) {
                array_unshift($uri, $model->parent->uri);
            }
            $model->uri = implode('/', $uri);

            // Если индикатор не выставлен, пробуем выставить автоматически
            if (!self::$updateUri) {
                self::$updateUri = $model->exists && ($model->isDirty('parent_id') || $model->isDirty('alias'));
            }
        });

        static::saved(static function (self $model) {
            // Проверяем необходимость обновления uri
            if (self::$updateUri) {
                // Обновляем дочерние товары
                foreach ($model->products()->cursor() as $product) {
                    $product->save();
                }

                // Рекурсивно обновляем дочерние категории
                foreach ($model->children()->cursor() as $category) {
                    $category::$updateUri = true;
                    $category->save();
                }
            }
        });
    }

    // ...
}
Здесь продолжается наша рекурсивная магия. Модель Category получает свойство updateUri, которое управляет обновлением uri дочерних товаров и категорий.
При обновлении дочерних категорий, это свойство выставляется им, и так будет до последнего уровня вложения, рекурсивно.
Обновление uri включается автоматичеси для уже существующих моделей, у которых изменён родитель или alias.
На мой вгляд, вполне себе симпатичное решение, о котором больше не нужно думать - все uri обновляются автоматически при сохранении модели.
Чтобы проверить, как это работает, я добавил генерацию события на обновления таблицы товаров из дерева, при перемещении категории.
Для загрузки товаров используется адрес API admin/products, поэтому событие для обновления таблицы будет 'app::admin-products::update':
<script>
export default {
  // ...
  methods: {
    async sortNodes({to, from, item, oldIndex, newIndex}) {
      // ...
      if (oldParent !== newParent || oldIndex !== newIndex) {
        // ...
        this.$root.$emit(`app::admin-products::update`)
      }
      this.$root.$emit(`app::categories-tree::sort`)
    },
  }
}
</script>
Проверяем работу:

Сортировка товаров

Хоть у нас и есть колонка rank у товаров, использовать её мы пока не будем. Честно говоря, я просто не представляю как это лучше сделать, ведь таскать товары по таблице с постраничной навигацией просто не получится.
Да и сама таблица может сортироваться по разным колонкам, например по артукулу или цене. То есть, порядок отображения строк не будет соответствовать rank.
Единственное боле-менее рабочее, что могу предложить, это добавить колонку сортировки по rank и кнопочки типа таких:
Типа, двинуть запись выше-ниже.
Но, по моему, это пустой труд. В публичной части всё равно будет вывод каталога с фильтрами и сортировкой по цене, так что и в админке ничего специально сортировать не будем. Но колонка rank на всякий случай пусть останется.

Заключение

Сегодня небольшая заметка, с доработкой мелочей, оставшихся с прошлого раза.
Дальше займёмся карточкой товара - в неё нужно добавить управление связями с другими товарами и подумать, как нам сделать работу с мультикатегориями.
Итоговый коммит с текущими изменениями.

Комментарии

bezumkin.ru
Personal website of Vasily Naumkin
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Futuris
04.04.2024, 08:56:12
Я просто немного запутался. Когда в абзаце "Vesp/Core" ты пишешь про "новый trait FileModel", я поду...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!