Хлебные крошки и мультикатегории
Сегодня продолжаем работать с выводом товаров на сайте - у нас осталась пара незаконченых дел.
Во-первых, у нас нет вывода товаров, вложенных в дочерние категории. И поддержки товаров, привязанных через ProductCategory тоже нет. Через это, например, большая категория /products/tabak выводится пустой.
Во-вторых, хлебные крошки умеют выводить только непосредственно родительскую категорию товара, без цепочки остальных категорий до корня сайта.
Надо это исправить.
Вывод товаров из мультикатегорий
Это вопрос решается довольно-таки просто, и он уже даже решён в админке - я просто забыл об этом рассказать.
Меняем условие вывода товаров категории в Controllers/Web/Category/Products.php:
protected function beforeCount(Builder $c): Builder
{
$c->where('active', true);
$c->where(function (Builder $c) {
// Товары категории
$c->where('category_id', $this->category->id);
// Товары мультикатегорий
$c->orWhereHas('productCategories', function (Builder $c) {
$c->where('category_id', $this->category->id);
});
});
return $c;
}
Получается вот такой запрос:
SELECT *
FROM `old_products`
WHERE `active` = '1'
and (
`category_id` = '1'
or exists (
SELECT *
FROM `old_product_categories`
WHERE `old_products`.`id` = `old_product_categories`.`product_id`
and `category_id` = '1'
)
)
ORDER BY `price` ASC
LIMIT 9 offset 0
На этом, вроде бы, всё. Но мне бы хотелось выводить не только вручную связанные с категорией товары, но и товары из её подкатегорий.
Сделать это можно только одним способом - получить все id активных подкатегорий и добавить их в условие запроса.
Метод это полезный, так что добавляем его напрямую в модель категории:
public static function getChildIds(int $parentId, bool $onlyActive = false): array {
$ids = [];
$children = self::query()->where('parent_id', $parentId)->select('id');
if ($onlyActive) {
$children->where('active', true);
}
foreach ($children->cursor() as $child) {
$ids = [...$ids, $child->id, ...self::getChildIds($child->id, $onlyActive)];
}
return $ids;
}
Это статический метод, который требует только указание id категории для рекурсивного получения массива c id всех её подкатегорий. Опционально можно выбрать только активные подкатегории - для публичного сайта.
Обновляем условие Web/Category/Products:
protected function beforeCount(Builder $c): Builder
{
$c->where('active', true);
$c->where(function (Builder $c) {
// Товары категории
$c->where('category_id', $this->category->id);
// Товары подкатегорий
if ($children = Category::getChildIds($this->category->id, true)) {
$c->orWhereIn('category_id', $children);
}
// Товары мультикатегорий
$c->orWhereHas('productCategories', function (Builder $c) {
$c->where('category_id', $this->category->id);
});
});
return $c;
}
С помощью Clockwork смотрим в итоговый SQL:
SELECT *
FROM `old_products`
WHERE `active` = '1'
and (
`category_id` = '1'
or `category_id` in ('11', '12', '13', '14', '10', '15')
or exists (
SELECT *
FROM `old_product_categories`
WHERE `old_products`.`id` = `old_product_categories`.`product_id`
and `category_id` = '1'
)
)
ORDER BY `price` ASC
LIMIT 9 offset 0
Насколько я вижу по мультикатегориям исходной базы miniShop2, владельцы как раз добавляли вручную товары подкатегорий в основную категорию.
Обновляем и контроллер админки Admin/Products:
protected function beforeCount(Builder $c): Builder
{
if ($category = (int)$this->getProperty('category')) {
$c->where(static function (Builder $c) use ($category) {
$c->where('category_id', $category);
if ($children = Category::getChildIds($category)) {
$c->orWhereIn('category_id', $children);
}
$c->orWhereHas('productCategories', static function (Builder $c) use ($category) {
$c->where('category_id', $category);
});
});
}
// ...
}
В админке будут выводиться товары всех категорий, включая неактивные.
Хлебные крошки
Вывод хлебных крошек от товара или категории - очень похожая задача, только здесь нужно получать массив с id родителей, а не потомков.
Создаём соответствующий метод в модели категорий:
public static function getParentIds(int $childId, bool $onlyActive = false): array
{
$ids = [];
$child = self::query()->where('id', $childId)->select('parent_id');
if ($onlyActive) {
$child->where('active', true);
}
if ($parentId = $child->value('parent_id')) {
$ids = [$parentId, ...self::getParentIds($parentId)];
}
return $ids;
}
Этот метод выдаст нам все id родителей категории, в порядке убывания уровня вложения. Опциональный второй параметр onlyActive позволяет остановить выборку, если один из родителей отключен.
Но для вывода хлебных крошек этой информации нам не хватит, ведь нужно подгрузить еще и переводы. Также хотелось бы не останавливать вывод категорий при нахождении неактивной, а просто прятать.
Пишем еще один метод:
public static function getBreadcrumbs(int $categoryId, bool $onlyActive = true): array
{
$ids = [$categoryId, ...self::getParentIds($categoryId)];
$breadcrumbs = self::query()
->select('id', 'uri')
->whereIn('id', $ids)
->with('translations:category_id,lang,title')
->orderByRaw('FIELD(id, ' . implode(',', $ids) . ')');
if ($onlyActive) {
$breadcrumbs->where('active', true);
}
return $breadcrumbs->get()->toArray();
}
Вот теперь мы выбираем всё нужное:
- включаем саму категорию, от которой идёт выборка
- выбираем только активные записи
- добираем переводы и uri
- сохраняем порядок записей от категории к корню сайта
В контроллере категорий мы вызываем эту функцию вот так:
public function prepareRow(Model $object): array
{
/** @var Category $object */
$array = $object->toArray();
if ($this->getPrimaryKey()) {
$array['breadcrumbs'] = Category::getBreadcrumbs($object->id);
}
return $array;
}
Метод prepareRow() позволяет модифицировать вывод данных на фронтенд для каждой записи, а проверка наличия getPrimaryKey() позволяет нам не делать ненужные выборки при выводе списка категорий.
Остаётся доработать наш компонент вывода хлебных крошек:
<template>
<b-breadcrumb>
<!-- Ссылка на корень каталога выводится всегда -->
<b-breadcrumb-item :to="home">
<fa icon="home" />
</b-breadcrumb-item>
<!-- Дальше выводим подготовленные крошки -->
<b-breadcrumb-item v-for="(i, idx) in breadcrumbs" :key="idx" :to="i.route" :active="i.active">
{{ i.title }}
</b-breadcrumb-item>
</b-breadcrumb>
</template>
<script>
export default {
name: 'Breadcrumbs',
props: {
// Этот параметр теперь требует массив с категориями, вместо объекта с одной категорией
categories: {
type: Array,
required: true,
},
product: {
type: Object,
default: null,
},
},
computed: {
// Ссылка на корень каталога зависит от нашей системной настройки
home() {
return '/' + this.$config.PRODUCTS_PREFIX
},
breadcrumbs() {
// Переворачиваем порядок массива категорий, чтобы было от корня к текущей категории
const res = [...this.categories].reverse().map((i, idx) => {
// Пробегаем по элементам и формируем данные
return {
id: i.id,
title: this.$translate(i.translations),
route: this.$productLink(i),
// Категория активна, если не указан товар и она последняя в массиве
active: !this.product && this.categories.length === idx + 1,
}
})
// Если указан товар
if (this.product) {
// Добавляем его в итоговый массив без ссылки
res.push({
id: this.product.id,
title: this.$translate(this.product.translations),
route: null,
active: true,
})
}
return res
},
},
}
</script>
Теперь обновляем вызов в компоненте категории:
<breadcrumbs v-if="category && category.breadcrumbs" :categories="category.breadcrumbs" />
И в компоненте товара:
<breadcrumbs v-if="product.breadcrumbs" :categories="product.breadcrumbs" :product="product" />
Проверяем результат:
Заключение
Вот мы и накрутили новых дополнительных запросов в БД, при этом на текущей базе я не вижу никаких тормозов.
Возможно, на огромном каталоге с десятком уровней вложения категорий могут возникнуть какие-то проблемы - но там можно и дописать промежуточное хранение дерева категорий в JSON файле, примерно как это сделано у MODX. Весь функционал вынесен в отдельные функции, так что будет несложно их доработать, при желании.
Ссылка на изменения в репозитории.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
178
29.08.2023, 07:35:42
Комментарии
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
inna
02.11.2024, 12:17:25
ой... по бесплатным урокам по vesp все ссылки битые. А так хотелось...
Vesp 3.0
102
Ivan CR
24.10.2024, 15:20:54
С днем рождения!!! Класс, что в твоей жизни есть такие интересные достижения.
День рождения 42
3
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Оплата заказа
2
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Обновление проекта
2
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
Создание нового проекта
63
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Поездка в Швейцарию
8
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
День рождения 41
6
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Релиз @vesp/nuxt-fontawesome
3
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.
Новая структура таблиц магазина
15
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Начинаем новый курс
4