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