Вывод товаров по uri
Сегодня мы обновляем публичный сайт с выводом товаров.
Нам нужно добавить работу с переводами, утилиты для вывода переводов и цен, а также научиться выводить товары по адресам с неизвестной глубиной вложения.
В предыдущей версии у нас вообще не было вложенных категорий, поэтому структура сайта была чётко прописана: категория и в ней товар. Сейчас же может быть категория\категория\категория\товар - и поэтому вывод товаров должен стать гораздо гибче.
Для начала просто починим вывод текущего каталога, который сломался из-за отсутствия колонки title - она теперь в отдельной таблице. Мы можем или присоединять таблицу переводов в контроллере, чтобы сохранить сортировку, или поменять её на дату публикации.
Я выбираю второй вариант, потому что сейчас нам нужно только починить вывод, а дальше мы его доработаем.
Меняем title на created_at в следующих файлах:
- site/pages/index.vue
- site/components/list-products-actions.vue
Подключаем утилиты
Товары выводятся, но у них нет названий. Это потому, что наш публичный сайт вообще не поддерживает мультиязычность.
Настраиваем site/nuxt.config.js:
- Подключаем все нужные модули
Config.modules = [...Config.modules, '@vesp/frontend']
- Раз мы подключили все модули, то настройку Config.Vesp можно просто удалить.
- Добавляем загрузку утилит из плагина админки, и создаём новый плагин для сайта
Config.plugins = ['../admin/plugins/utils.js', '~/plugins/utils.js']
- Копируем настройки i18n из конфига админки
Config.i18n.vueI18n = '@/lexicons/index.js' Config.i18n.defaultLocale = 'ru' Config.i18n.locales = [ {code: 'ru', title: 'Русский'}, {code: 'en', title: 'English'}, ]
- Создаём соответствующие файлы переводов, в которых просто реэкспортируем данные из админки.
frontend/src/site/lexicons/ru.js:
import Ru from '../../admin/lexicons/ru.js'
export default Ru
И создаём пустой плагин сайта:
frontend/src/site/plugins/utils.js:
export default ({app}, inject) => {
}
Смысл в том, чтобы использовать ту же логику, что и в адмике, без дублирования. А всё новое мы напишем в свой плагин.
Перед выводом названий товаров, нужно их подключить в контроллере Controllers/Web/Products.php.
protected function beforeGet(Builder $c): Builder
{
// ...
$c->with('translations');
$c->with('category:id,uri,active', 'category.translations:category_id,lang,title');
return $c;
}
protected function afterCount(Builder $c): Builder
{
$c->with('translations:product_id,lang,title');
$c->with('category:id,uri,active', 'category.translations:category_id,lang,title');
$c->with('firstFile');
return $c;
}
Теперь мы можем использовать методы $translate и $price в компоненте components/list-products.vue.
Названия и цены выводятся как в админке. Остальные элементы страницы пока не переводим.
Универсальный контроллер
В отличие от предыдущей версии, мы не знаем возможную глубину вложения категорий, она ничем не ограничена, поэтому нам нужен универсальный маршрут.
Согласно документации Nuxt 2, такой маршрут создаётся файлом с именем _.vue.
Чтобы отделить вывод товаров магазина от других страниц, предлагаю добавить его в директорию products:
<template>
<div>
{{ $route.name }}
{{ $route.params }}
</div>
</template>
<script>
export default {
name: 'ProductsDynamicPage',
}
</script>
Дальше на этой странице мы можем прописать всё нужное для загрузки товаров магазина.
Чтобы не вспоминать, на какой виртуальной странице у нас выводятся товары, я добавил в .env новую переменную PRODUCTS_PREFIX=products, которую буду использовать для генерации ссылок на конечные товары и категории.
Создаём метод productLink в утилитах:
export default ({app}, inject) => {
inject('productLink', ({uri}) => {
return uri ? ['', app.$config.PRODUCTS_PREFIX, uri].join('/') : ''
})
}
И теперь с главной страницы мы будем попадать на новую страницу товара, используя новую функцию
<b-link :to="$productLink(product)" ...>
Из этого адреса мы не cможем понять, запрашивается категория или товар. Поэтому вместо запроса конкретного контроллера из API нам нужен какой-то универсальный, который выдаст ответ только по uri.
Пишем очень интересный Controllers/Web/Resource.php:
<?php
namespace App\Controllers\Web;
use App\Models\Category;
use App\Models\Product;
use Psr\Http\Message\ResponseInterface;
use Slim\App;
use Slim\Psr7\Factory\ServerRequestFactory;
use Vesp\Controllers\Controller;
use Vesp\Services\Eloquent;
class Resource extends Controller
{
private App $app;
// Используем dependency injection, чтобы получить
// экземпяр текущего приложения
public function __construct(App $app, Eloquent $eloquent)
{
parent::__construct($eloquent);
$this->app = $app;
}
public function get(): ResponseInterface
{
// Получаем основные параметры
$uri = trim($this->getProperty('uri', ''), '/');
$endpoint = trim($this->request->getRequestTarget(), '/');
// Инициализируем новую фабрику для запросов
$factory = new ServerRequestFactory();
// Проверяем, есть ли товар с таким uri, или категория
if (Product::query()->where('uri', $uri)->count()) {
$endpoint = str_replace('/resource/', '/products/', $endpoint);
$request = $factory->createServerRequest('GET', $endpoint);
} elseif (Category::query()->where('uri', $uri)->count()) {
$endpoint = str_replace('/resource/', '/categories/', $endpoint);
$request = $factory->createServerRequest('GET', $endpoint);
} else {
// Если ни товара, ни категории не найдено - выходим
return $this->failure('Not Found', 404);
}
// Добавляем полученные заголовки в новый запрос
foreach ($this->request->getHeaders() as $key => $value) {
$request = $request->withHeader($key, $value);
}
// И передаём приложению наш запрос на обработку
return $this->app->handle($request);
}
}
Как видно, здесь мы проверяем наличие товаров и категорий по uri и подменяем запрос для загрузки нужного контроллера, делая вид, что обращение было по его адресу. Очень просто и красиво.
Контроллеры Resource, Products и Categories теперь должны принимать uri с качестве параметра. Причём, uri должен работать вместе со всеми слешами.
Находим, как указать такое в документации FastRoute - это {uri:.+}:
$group->group(
'/web',
static function (RouteCollectorProxy $group) {
$group->any('/resource/{uri:.+}', App\Controllers\Web\Resource::class);
$group->any('/categories[/{uri:.+}]', App\Controllers\Web\Categories::class);
$group->any('/category/{category_id}/products', App\Controllers\Web\Category\Products::class);
$group->any('/products[/{uri:.+}]', App\Controllers\Web\Products::class);
$group->any('/orders', App\Controllers\Web\Orders::class);
}
);
Контроллер Resource требует обязательного указания uri, а у Categories и Products оно опционально.
Заодно я поменял маршрут для вывода товаров категории на более логичный, плюс, он не будет путаться с общим адресом категорий.
Меняем в 2х контроллерах определение первичного ключа:
protected function getPrimaryKey(): ?array
{
if ($uri = $this->getProperty('uri')) {
return ['uri' => $uri];
}
return null;
}
Бэкенд готов для запросов - мы получаем разные данные, в зависимости от запрошенного uri.
Вывод товара
Редактируем нашу универсальную страницу, добавляя загрузку данных:
export default {
name: 'ProductsDynamicPage',
validate({params, route, redirect}) {
// Если запрос заканчивается на слэш, редиректим на адрес без слэша
if (route.fullPath.endsWith('/')) {
return redirect(route.fullPath.slice(0, -1))
}
// И проверяем, есть ли вообще какой-то uri
return Boolean(params.pathMatch)
},
async asyncData({app, params, error}) {
// Запрос в API
try {
const {data} = await app.$axios.get('web/resource/' + params.pathMatch)
return {record: data}
} catch (e) {
error({statusCode: e.response.status, message: e.message})
}
},
data() {
return {
record: {},
}
},
computed: {
isCategory() {
return this.record.category_id === undefined
},
},
}
Шаблон меняем для вывод этих данных
<template>
<div>
{{ String(isCategory) }}
<pre>{{ record }}</pre>
</div>
</template>
Проверяем:
Дальше дело техники - рендерить разное содержимое страницы, в зависимости от того, категория это или товар.
Вывод товаров
Выносим содержимое страницы товара в новый компонент site/components/product.vue с небольшими изменениями: добавляем вывод переводов и правильных ссылок. Также меняем и компонент breadcrumbs.
Шаблон универсальной страницы теперь выглядит так:
<template>
<div>
<product v-if="!isCategory" :product="record" />
<pre v-else>{{ record }}</pre>
</div>
</template>
Подобным образом выносим и pages/_category/index.vue в отдельный копонент category.
<template>
<div>
<breadcrumbs v-if="category" :category="category" />
<list-products-actions v-model="listView" :sort.sync="sort" :dir.sync="dir" />
<list-products
v-model="page"
:category="category ? category.id : null"
:limit="limit"
:sort="sort"
:dir="dir"
:list-view="listView"
@load="onLoad"
/>
<b-pagination v-if="total > limit" v-model="page" :total-rows="total" :per-page="limit" class="mt-5" />
</div>
</template>
Делаем проверку на наличие категории при выводе хлебных крошек, чтобы можно было вызывать <category /> и на главной странице:
<template>
<category />
</template>
<script>
import Category from '~/components/category.vue'
export default {
name: 'IndexPage',
components: {Category},
}
</script>
Директорию pages/_category со вложенными файлами можно удалять.
На сайте осталось всего 2 страницы:
- pages/index.vue
- pages/products/_.vue
И этого пока достаточно для вывода всего каталога.
Если мы напрямую зайдём на страницу /products, то будет ошибка, потому что uri не указан. Чтобы такого не случилось, копируем index.vue внутрь pages/products - получается 3я страница:
- pages/products/index.vue
Она будет корневой для каталога, тем более, что главную страницу обычно используют для вывода новостей, акций и всего такого.
Заключение
Я не стал описывать вообще все изменения, но вы сможете найти их в репозитории. В основном они касаются вывода переводов там и сям.
В итоге мы починили вывод товаров и научились работать с бесконечной глубиной вложения категорий.
Остаётся еще вопрос, как выводить товары мультикатегории, и на какую грлубину выводить товары из подкатегорий - но это оставим на потом.
Итоговый коммит в репозитории.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
167
28.08.2023, 11:46:56
3 комментария
Дмитрий П.
07.09.2023, 13:29:20
А так должно быть, что через контроллер Resource делается много повторяющихся запросов в базу, нежели чем через обычный контроллер?
Вот запросы контроллера Resource
А вот это уже контроллера Categoreis
Василий Наумкин
07.09.2023, 13:59:12
Судя по всему, это какой-то глюк Clockwork, из-за хитрой логики обработки контроллера.
Потому что видно, что одинаковые запросы повторяются несколько раз, но если врубить Xdebug, и поставить точку на один из методов контроллера категорий - то получает, что контроллер срабатывает только один раз.
Вот, записал на видео - через Xdebug одна остановка за обработку.
Screen Recording 2023-09-07 at 17.55.47.mov
7.61 MB, video/quicktime
Дмитрий П.
07.09.2023, 14:19:55
ну да, что-то на этот момент я не обратил внимание)
видимо дублирование в девтулсах происходит из-за того, что при перенаправлении запроса Middlewere-ы, среди которых и Clockwork, тоже запускаются второй раз
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.