Сортировка моделей и генерация 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 на всякий случай пусть останется.

Заключение

Сегодня небольшая заметка, с доработкой мелочей, оставшихся с прошлого раза.

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

Итоговый коммит с текущими изменениями.

← Предыдущая заметка
Рекурсивное дерево категорий
Следующая заметка →
Связи товара и мультикатегории
Комментарии (0)
bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце &quot;Vesp/Core&quot; ты пишешь про &quot;новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...