Вывод товаров на сайте
На прошлом уроке мы доработали наш вывод товаров, добавив к нему выборку картинок.
Теперь нужно сделать вывод категорий и страниц товаров. Учитывая общепринятые нормы, мы не можем выводить их все просто по id, поэтому давайте добавим колонки alias обеим моделям.
У категорий все alias будут уникальны, а у товаров они будут уникальны в каждой категории.
Alias становится обязательным полем для модели, то есть он всегда должен быть заполнен. Это значит, что колонка в таблице не будет nullable, а поэтому при создании уникального индекса по этой колонки в ней уже должны быть уникальные значения, или получим ошибку.
Если бы наш магазин уже был в работе, то нам нужно было бы добавить новую колонку, затем прописать уникальные значения в каждую модель, и только потом добавлять индекс unique(). Но сейчас мы можем смело удалить все записи из БД, а после запуска миграции создать их снова через запуск seed.
Создаём новую миграцию composer db:create Aliases и редактируем:
<?php
declare(strict_types=1);
use App\Models\Category;
use App\Models\Product;
use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Eloquent;
use Vesp\Services\Migration;
final class Aliases extends Migration
{
public function up(): void
{
// Отключаем проверку связей с другими таблицами
(new Eloquent())->getConnection()->getSchemaBuilder()->disableForeignKeyConstraints();
// И удаляем все товары с категориями
Category::query()->truncate();
Product::query()->truncate();
// Меняем таблицу категорий
$this->schema->table(
'categories',
function (Blueprint $table) {
// И добавляем уникальную колонку alias сразу за колонкой description
$table->string('alias')->after('description')->unique();
}
);
// Затем меняем и таблицу товаров
$this->schema->table(
'products',
function (Blueprint $table) {
// Добавляем новую колонку
$table->string('alias')->after('description');
// И отдельно составной уникальный индекс
$table->unique(['category_id', 'alias']);
}
);
}
// При откате миграции нужно привести всё к виду "как было до неё"
public function down(): void
{
$this->schema->table(
'products',
function (Blueprint $table) {
// Новый составной уникальный индекс у товаров расширяет старый ключ по категориям
// Поэтому удаляем и ключ
$table->dropForeign(['category_id']);
// И уникальный индекс, как будто их и не было
$table->dropUnique(['category_id', 'alias']);
// Удаляем новую колонку
$table->dropColumn('alias');
// И создаём заново ключ по категориям
$table->foreign('category_id')->references('id')
->on('categories')->cascadeOnDelete();
}
);
// У категорий же всё проще
$this->schema->table(
'categories',
function (Blueprint $table) {
// Просто удаляем колонку, при этом удалится и её индекс
$table->dropColumn('alias');
}
);
}
}
Правильностью работы миграции я считаю возможность запустить migrate, а затем rollback и снова migrate без ошибок. Это будет означать, что миграция правильно откатывает все свои изменения.
Так что делаем
composer db:migrate
composer db:rollback
composer db:migrate
И видим, что всё в порядке.
Теперь нужно добавить alias в наш seed товаров - редактируем core/db/seeds/Products.php:
<?php
// ...
class Products extends AbstractSeed
{
// ...
public function run(): void
{
// ...
for ($i = 0; $i < $this->categories; $i++) {
Category::query()->create([
'title' => 'Категория ' . ($i + 1),
// Псевдоним категории
'alias' => 'category-' . ($i + 1),
// ...
]);
}
for ($i = 0; $i < $this->products; $i++) {
Product::query()->create([
'title' => 'Товар ' . ($i + 1),
'description' => $faker->text,
// Псевдоним товара
'alias' => 'product-' . ($i + 1),
// ...
]);
}
}
}
Файлик привожу в сокращённом виде, тут всё понятно - просто забиваем значение в новую колонку.
В моделях Category и Product ничего менять не нужно, только прописать новое свойство для автодополнения в IDE:
* @property string $alias
Работаем с alias в админке
Тут тоже всё просто - добавляем новое поле в формы frontend/src/admin/components/forms/category.vue:
<b-form-group :label="$t('models.category.alias')">
<b-form-input v-model.trim="record.alias" required />
</b-form-group>
и frontend/src/admin/components/forms/product.vue:
<b-form-group :label="$t('models.product.alias')">
<b-form-input v-model.trim="record.alias" required />
</b-form-group>
А затем соответствующие записи в словарях админки frontend/src/admin/lexicons для категории и товара:
alias: 'Псевдоним',
И всё у нас работает:
Уникальность псевдонима сейчас проверяется на стороне базы данных, поэтому если мы укажем неуникальный псевдоним для категории, получим вот такую некрасивую ошибку:
Поэтому я предлагаю нам самим проверять уникальность псевдонима в контроллере core/src/Controllers/Admin/Categories.php в методе beforeSave(), который и предназначен для подобных проверок:
protected function beforeSave(Model $record): ?ResponseInterface
{
// Создаём новый запрос в БД, с указанным alias модели
$c = Category::query()->where('alias', $record->alias);
// Если эта модель уже существует, то есть это не создание, а редактирование
// Добавляем еще проверку по id
if ($record->exists) {
$c->where('id', '!=', $record->id);
}
// И если в БД уже есть модель с другим id, но таким же alias
if ($c->count()) {
// Возвращаем ошибку в виде ключа из лексиконов
return $this->failure('errors.category.alias_exists');
}
return null;
}
Остаётся только добавить соответствующие сообщения в наши словари frontend/src/admin/lexicons:
// ...
errors: {
// ...
category: {
alias_exists: 'Такой псевдоним уже существует, укажите другой',
}
},
Проверяем наши изменения:
По большому счёту мы просто украсили уже работающий механизм проверки уникальности колонки на стороне базы данных. Ровно так же нужно прописать и проверку для товаров, только с указанием id категории.
Редактируем core/src/Controllers/Admin/Products.php:
protected function beforeSave(Model $record): ?ResponseInterface
{
// Вот здесь добавляется второе условие
$c = Product::query()->where(['alias' => $record->alias, 'category_id' => $record->category_id]);
if ($record->exists) {
$c->where('id', '!=', $record->id);
}
if ($c->count()) {
// Ну и меняется ключ ошибки
return $this->failure('errors.product.alias_exists');
}
return null;
}
И не забываем обновить словари с указанием новой ошибки для товаров.
Создаём страницы товаров на сайте
Так как мы делаем ненастоящий магазин в целях обучения, я предлагаю не заморачиваться по с отдельным выводом категорий, общего меню сайта и прочего.
Мы оставляем тот же общий вывод товаров на главной странице, но приделываем для каждого товара собственную страницу, которая будет внутри страницы его категории.
Структура получается такая:
Главная со всеми товарами
- Страница категории товара, с выводом всех её товаров
- Страница конкретного товара
Согласно правилам роутинга NuxtJs получается такая структура файлов:
pages/
_category/
_product/
index.vue - динамическая страница товара
index.vue - динамическая страница категории
index.vue - главная страница
Указание имён каталогов с ведущим подчёркиванием делает их динамическими. Давайте пропишем в 2х новых файлах index.vue такой код для отладки:
<template>
<div>{{ $route.params }}</div>
</template>
Теперь при переходе по любым адресам до 2х уровней вложенности мы будем видеть рабочие страницы и переданные в URL параметры:
Теперь нам нужно доработать эти страницы так, чтобы категории выводили свои товары, а страница товаров - данные о нём. Сегодня разбираем принцип работы, пока без оформления.
Обновляем публичные контроллеры
Раз наши категории и товары теперь будут запрашиваться не по id, расширяем метод определения первичного ключа в публичном контроллере категории core/src/Controllers/Web/Categories.php. У категорий alias уникален и будет заменять id:
protected function getPrimaryKey(): ?array
{
// Если указан параметр alias, то это запрос одной модели
if ($alias = $this->getProperty('alias')) {
// И мы возвращаем массив, по которому эту модель можно выбрать
return ['alias' => $alias];
}
return null;
}
А вот с товарами ситуация сложнее, ведь у них alias уникален только в пределах одной категории, значит нам обязательно нужно передавать и её alias при запросе.
Я предлагаю разбить функционал текущего контроллера товаров на 2 контроллера.
Первый Controllers/Web/Products.php оставляем на месте и вносим всего 2 изменения
protected function afterCount(Builder $c): Builder
{
// Добавляем категориям выборку их alias
$c->with('category:id,title,alias');
$c->with('firstFile');
return $c;
}
// Первичный ключ всегда null, а значит работать будет только вывод товаров списком
protected function getPrimaryKey(): ?array
{
return null;
}
А для вывода товаров категории, как списком, так и поштучно, создаём новый контроллер по адресу core/src/Controllers/Web/Category/Products.php:
<?php
namespace App\Controllers\Web\Category;
use App\Models\Category;
use Illuminate\Database\Eloquent\Builder;
use Psr\Http\Message\ResponseInterface;
// Новый контроллер расширяет общий контроллер товаров
class Products extends \App\Controllers\Web\Products
{
/** @var Category $category */
protected $category;
// Загружаем категорию, так же как в админке у контроллера файлов загружали товар
public function checkScope(string $method): ?ResponseInterface
{
// Категория должна быть активна
$c = Category::query()->where(['active' => true, 'alias' => $this->getProperty('category')]);
if (!$this->category = $c->first()) {
return $this->failure('', 404);
}
return null;
}
protected function beforeGet(Builder $c): Builder
{
// Все получаемые товары должны быть активны
$c->where('active', true);
$c->with('category:id,title,alias');
return $c;
}
protected function beforeCount(Builder $c): Builder
{
// Список моделей выбирается только для указанной категории
$c->where(['active' => true, 'category_id' => $this->category->id]);
return $c;
}
protected function getPrimaryKey(): ?array
{
// Если указан alias товара, то это запрос одной модели, и мы выбираем её с учётом
// загруженной категории
if ($alias = $this->getProperty('alias')) {
return ['alias' => $alias, 'category_id' => $this->category->id];
}
return null;
}
}
Мы расширяем основной контроллер товаров и заменяем нужные нам методы.
И затем обновляем наши публичные маршруты core/routes.php, чтобы передавать в них правильные параметры для контроллеров:
$group->group(
'/web',
static function (RouteCollectorProxy $group) {
// Вместо id теперь alias
$group->any('/categories[/{alias}]', App\Controllers\Web\Categories::class);
// Новый маршрут для выборки товаров категории
// Как списком, так и поштучно
$group->any('/categories/{category}/products[/{alias}]', App\Controllers\Web\Category\Products::class);
// Оставляем общую выборку товаров, но без возможности получаения отдельной позиции
$group->any('/products', App\Controllers\Web\Products::class);
}
);
Теперь для получения данных одного товара нам обязательно нужно обращаться в его категорию. Выборки по id больше не работают, только по alias. При этом осталась возможность получить все активные товары из опубликованных категорий, одним большим списком для главной страницы.
Проверяем новые адреса:
api/web/categories выводит список всех активных категорий
api/web/categories/alias-категории выводит саму категорию
api/web/categories/alias-категории/products выводит список товаров категории
api/web/categories/alias-категории/products/alias-товара выведет один товар категории
Всё просто и логично. Осталось только вывести эти данные на фронтенде.
Вывод товаров на сайте
Первым делом проставляем ссылки на страницу товара в нашем компоненте frontend/src/site/components/list-products.vue. Для этого мы параметры ссылки выносим в отдельный метод getLink(), который используется при оформлении:
<template>
<b-overlay :show="loading" opacity="0.5">
<!-- -->
<b-link :to="getLink(product)">
<div class="image mr-2">
<!-- -->
</div>
</b-link>
<!-- -->
<b-link :to="getLink(product)" class="font-weight-bold">{{ product.title }}</b-link>
<!-- -->
</b-overlay>
</template>
<script>
//...
methods: {
getLink(product) {
return {name: 'category-product', params: {category: product.category.alias, product: product.alias}}
},
},
}
Использование отдельного метода для ссылок гораздо удобнее, чем прописывать их в шаблоне в каждом нужно месте. Тем более, что ссылки могут иногда меняться.
Теперь при клике на картинку или название товара мы попадём на его страницу, где нам нужно будет загрузить данные. Редактируем frontend/src/site/pages/_category/_product/index.vue:
<template>
<div class="mt-5">
<!-- Ссылка на главную страницу-->
<b-link :to="{name: 'index'}">← Вернуться назад</b-link>
<!-- Пока что просто печатаем загруженные данные -->
<pre class="mt-3">{{ product }}</pre>
</div>
</template>
<script>
export default {
name: 'ProductPage',
// Проверяем, чтобы были присланы alias категории и товара
validate({params}) {
return params.category && params.product
},
// И получаем данные с сервера
async asyncData({app, params, error}) {
try {
// Формируем правильный адрес до коннектора
const {data} = await app.$axios.get('web/categories/' + params.category + '/products/' + params.product)
// Выставляем полученные данные на страницу
return {product: data}
} catch (e) {
error({statusCode: e.response.status, message: e.message})
}
},
data() {
return {
product: {},
}
},
}
</script>
Чтобы правильно выводить текст ошибки измените frontend/src/site/nuxt.config.js таким образом:
Config.Vesp = {
components: false,
scss: false,
i18n: false,
axios: false, // Вот здесь нужно поменять на false
utils: true,
filters: false,
}
Этим мы отключим плагин Vesp для обработки ошибок, который рассчитывает на всякие, на данный момент, у нас отключенные штуки. Надо бы мне его доработать...
Заключение
Вот мы и вывели наши товары, пока что без оформления, но как вы понимаете - это дело техники. Добавляем нужные данные для выборки в контроллере и оформляем на странице, ничего сложного.
На следующем занятии мы этим и займёмся, вместе со страницей категории. Все изменения этого урока можно найти на Github.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
340
28.06.2022, 18:22:21
20 комментариев
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Дмитрий
21.12.2024, 13:27:06
Здравствуйте.В ModX есть полезная функция "заморозить url родителя". При ее включении вместо:
УРЛ п...
Вывод товаров на сайте
20
Дмитрий
14.12.2024, 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Василий Наумкин
05.12.2024, 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!
В ModX есть полезная функция "заморозить url родителя". При ее включении вместо: