Магазин на Laravel 7, часть 25. Поиск по каталогу товаров, деплой проекта на хостинг TimeWeb
29.11.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Класс • Поиск • Практика • Таблица • Форма • ШаблонСайта
Поиск по каталогу товаров
Какой каталог товаров без поиска? Тем более, что и форма у нас уже есть. Искать будем по полям name
и content
таблицы products
, полю name
таблицы categories
и полю name
таблицы brands
. У нас должен получиться примерно такой SQL-запрос для поиска «мужская зимняя обувь».
SELECT `products`.*, IF (`products`.`name` LIKE '%мужск%', 2, 0) + IF (`products`.`content` LIKE '%мужск%', 1, 0) + IF (`categories`.`name` LIKE '%мужск%', 1, 0) + IF (`brands`.`name` LIKE '%мужск%', 2, 0) + IF (`products`.`name` LIKE '%зимн%', 2, 0) + IF (`products`.`content` LIKE '%зимн%', 1, 0) + IF (`categories`.`name` LIKE '%зимн%', 1, 0) + IF (`brands`.`name` LIKE '%зимн%', 2, 0) + IF (`products`.`name` LIKE '%обув%', 2, 0) + IF (`products`.`content` LIKE '%обув%', 1, 0) + IF (`categories`.`name` LIKE '%обув%', 1, 0) + IF (`brands`.`name` LIKE '%обув%', 2, 0) AS `relevance` FROM `products` INNER JOIN `categories` ON `categories`.`id` = `products`.`category_id` INNER JOIN `brands` ON `brands`.`id` = `products`.`brand_id` WHERE `products`.`name` LIKE '%мужск%' OR `products`.`content` LIKE '%мужск%' OR `categories`.`name` LIKE '%мужск%' OR `brands`.`name` LIKE '%мужск%' OR `products`.`name` LIKE '%зимн%' OR `products`.`content` LIKE '%зимн%' OR `categories`.`name` LIKE '%зимн%' OR `brands`.`name` LIKE '%зимн%' OR `products`.`name` LIKE '%обув%' OR `products`.`content` LIKE '%обув%' OR `categories`.`name` LIKE '%обув%' OR `brands`.`name` LIKE '%обув%' ORDER BY `relevance` DESC
Чтобы решить проблему окончания слов — используем стеммер Портера, который отсекает лишнее, оставляя только корень. Мне удалось найти на packagist.org готовый класс стеммера для русского языка.
Стеммер Портера — алгоритм стемминга, опубликованный Мартином Портером в 1980 году. Оригинальная версия стеммера была предназначена для английского языка. Впоследствии Мартин создал проект «Snowball» и, используя основную идею алгоритма, написал стеммеры для распространённых индоевропейских языков, в том числе для русского.
Алгоритм не использует морфологический словарь, а только применяя последовательно ряд правил, отсекает окончания и суффиксы, основываясь на особенностях языка, в связи с чем работает быстро, но не всегда безошибочно.
Итак, устанавливаем пакет с использованием composer:
> composer require ladamalina/lingua-stem-ru
Все, теперь нам доступен класс LinguaStemRu
. Пример использования класса:
$stemmer = new LinguaStemRu(); echo $stemmer->stem_text('Любовь к Родине – это очень сильное чувство');
любов к родин – это очен сильн чувство
Добавляем новый маршрут в файле route/web.php
:
/* * Каталог товаров: категория, бренд и товар */ Route::group([ 'as' => 'catalog.', // имя маршрута, например catalog.index 'prefix' => 'catalog', // префикс маршрута, например catalog/index ], function () { // главная страница каталога Route::get('index', 'CatalogController@index') ->name('index'); // категория каталога товаров Route::get('category/{category:slug}', 'CatalogController@category') ->name('category'); // бренд каталога товаров Route::get('brand/{brand:slug}', 'CatalogController@brand') ->name('brand'); // страница товара каталога Route::get('product/{product:slug}', 'CatalogController@product') ->name('product'); // страница результатов поиска Route::get('search', 'CatalogController@search') ->name('search'); });
Изменяем форму поиска в layout-шаблоне site.blade.php
:
<form action="{{ route('catalog.search') }}" class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" name="query" placeholder="Поиск по каталогу" aria-label="Search"> <button class="btn btn-outline-light my-2 my-sm-0" type="submit">Поиск</button> </form>
Добавляем новый метод search()
в контроллер CatalogController
:
class CatalogController extends Controller { /* ... */ public function search(Request $request) { $search = $request->input('query'); $query = Product::search($search); $products = $query->paginate(6)->withQueryString(); return view('catalog.search', compact('products', 'search')); } }
Добавляем новый метод scopeSearch()
в модель Product
:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Stem\LinguaStemRu; class Product extends Model { /** * Позволяет искать товары по заданным словам * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $search * @return \Illuminate\Database\Eloquent\Builder */ public function scopeSearch($query, $search) { // обрезаем поисковый запрос $search = iconv_substr($search, 0, 64); // удаляем все, кроме букв и цифр $search = preg_replace('#[^0-9a-zA-ZА-Яа-яёЁ]#u', ' ', $search); // сжимаем двойные пробелы $search = preg_replace('#\s+#u', ' ', $search); $search = trim($search); if (empty($search)) { return $query->whereNull('id'); // возвращаем пустой результат } // разбиваем поисковый запрос на отдельные слова $temp = explode(' ', $search); $words = []; $stemmer = new LinguaStemRu(); foreach ($temp as $item) { if (iconv_strlen($item) > 3) { // получаем корень слова $words[] = $stemmer->stem_word($item); } else { $words[] = $item; } } $relevance = "IF (`products`.`name` LIKE '%" . $words[0] . "%', 2, 0)"; $relevance .= " + IF (`products`.`content` LIKE '%" . $words[0] . "%', 1, 0)"; $relevance .= " + IF (`categories`.`name` LIKE '%" . $words[0] . "%', 1, 0)"; $relevance .= " + IF (`brands`.`name` LIKE '%" . $words[0] . "%', 2, 0)"; for ($i = 1; $i < count($words); $i++) { $relevance .= " + IF (`products`.`name` LIKE '%" . $words[$i] . "%', 2, 0)"; $relevance .= " + IF (`products`.`content` LIKE '%" . $words[$i] . "%', 1, 0)"; $relevance .= " + IF (`categories`.`name` LIKE '%" . $words[$i] . "%', 1, 0)"; $relevance .= " + IF (`brands`.`name` LIKE '%" . $words[$i] . "%', 2, 0)"; } $query->join('categories', 'categories.id', '=', 'products.category_id') ->join('brands', 'brands.id', '=', 'products.brand_id') ->select('products.*', DB::raw($relevance . ' as relevance')) ->where('products.name', 'like', '%' . $words[0] . '%') ->orWhere('products.content', 'like', '%' . $words[0] . '%') ->orWhere('categories.name', 'like', '%' . $words[0] . '%') ->orWhere('brands.name', 'like', '%' . $words[0] . '%'); for ($i = 1; $i < count($words); $i++) { $query = $query->orWhere('products.name', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('products.content', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('categories.name', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('brands.name', 'like', '%' . $words[$i] . '%'); } $query->orderBy('relevance', 'desc'); return $query; } }
Искать будем по следующим полям таблиц базы данных:
- поле
name
таблицыproducts
(название товара) - поле
content
таблицыproducts
(описание товара) - поле
name
таблицыcategories
(название категории) - поле
name
таблицыbrands
(название бренда)
Причем вклад в релевантность у них разный — у названия товара и названия бренда больше, у названия категории и описания товара меньше.
Осталось только создать шаблон views/catalog/search.blade.php
:
@extends('layout.site', ['title' => 'Поиск по каталогу']) @section('content') <h1>Поиск по каталогу</h1> <p>Поисковый запрос: {{ $search ?? 'пусто' }}</p> @if (count($products)) <div class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach </div> {{ $products->links() }} @else <p>По вашему запросу ничего не найдено</p> @endif @endsection
Деплой на хостинг TimeWeb
1. Создаем репозиторий на GitHub
Сначала создадим новый репозиторий на GitHub, у меня это laravel-7-shop
:
Сразу после создания нового репозитория получим подсказки, что делать дальше.
На локальном компьютере тоже создаем репозиторой, чтобы выложить на GitHub:
$ cd D:/work/localhost21/www # это директория проекта
$ git init # создаем пустой репозиторий Initialized empty Git repository in D:/work/localhost21/www/.git/ $ git add --all # добавляем все файлы проекта $ git commit -m "Initial сommit" # первый коммит
У нас уже есть подсказка от GitHub, что надо сделать, чтобы выложить проект (не торопитесь выполнять):
$ git remote add origin https://github.com/tokmakov/laravel-7-shop.git $ git branch -M main # изменить имя с master на main $ git push -u origin main
master
на main
. Потому что имя master
им представляется недостаточно толерантным. Но мы не будем следовать сомнительным советам, имя master
вполне подходит.
Добавляем себе ссылку (pointer) на удаленный репозиторий:
$ git remote add origin https://github.com/tokmakov/laravel-7-shop.git
Теперь удаленный репозиторий доступен как origin
:
$ git remote origin
Отправляем текущую ветку master
(которая у нас сейчас всего одна) в удаленную ветку master
репозитория origin
:
$ git push origin master
Настроим текущую ветку master
на отслеживание удаленной ветки master
:
$ git branch -u origin/master Branch master set up to track remote branch master from origin.
2. Клонируем проект на сервере
В панели управления хостинга нужно включить доступ по SSH и подключиться из командной строки к серверу. На хостинге TimeWeb уже установлен Composer, только нужно задать алиасы (см. здесь) для composer
и php
.
$ pwd /home/c/ca12345 $ nano .bash_profile
alias composer='/opt/php72/bin/php -d memory_limit=1024M /usr/local/bin/composer' alias php='/opt/php72/bin/php -d memory_limit=1024M'
Чтобы применить изменения из файла .bash_profile
:
$ source ~/.bash_profile
Проверяем, что Composer нам теперь доступен:
$ composer ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 1.9.0 2019-08-02 20:55:32 Usage: command [options] [arguments] ..........
Создаем директорию для нашего проекта в панели управления хостингом, у меня это laravel-7-shop
. Внутри будет автоматически создана директория public_html
, которая является корнем сервера. Нам это директория не нужна, потому что у нас корень сервера — директория public
.
$ rm -r ~/laravel-7-shop/public_html
Клонируем в laravel-7-shop
репозиторий с GitHub:
$ git clone https://github.com/tokmakov/laravel-7-shop.git laravel-7-shop
3. Устанавливаем проект на сервере
Устанавливаем проект с помощью Composer:
$ cd laravel-7-shop $ composer install --no-dev Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 122 installs, 0 updates, 0 removals - Installing phpdocumentor/reflection-common (2.2.0): Downloading (100%) - Installing phpdocumentor/type-resolver (1.4.0): Downloading (100%) .......... Discovered Package: nunomaduro/collision Package manifest generated successfully.
4. Создаем новую базу данных
В панели управления хостингом создаем новую базу данных, у меня это ca12345_lar7shop
. Создаем файл конфигурации приложения, указываем имя БД, имя пользователя БД и пароль.
$ cp .env.example .env $ nano .env
APP_NAME="Магазин одежды и обуви" APP_ENV=production APP_KEY= APP_DEBUG=false APP_URL=http://laravel-7-shop.tokmakov.msk.ru LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=ca12345_lar7shop DB_USERNAME=ca12345_lar7shop DB_PASSWORD=FapeTZ61
Создаем ключ приложения Laravel:
$ php artisan key:generate Application key set successfully.
5. Создаем таблицы базы данных
Запускаем миграцию, чтобы создать таблицы БД и тестовые данные:
$ php artisan migrate:fresh --seed ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Illuminate\Database\QueryException SQLSTATE[HY000]: General error: 1709 Index column size too large. The maximum column size is 767 bytes. (SQL: alter table `users` add unique `users_email_unique`(`email`))
Исправить это легко, изменяем файл AppServiceProvider.php
:
class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { Schema::defaultStringLength(191); } /* ... */ }
Выкладываем изменения на GitHub:
$ git commit -a -m "default string length" $ git push
На сервере получаем изменения:
$ git pull
Снова запускаем миграцию и получаем следующую порцию ошибок, что класс App\Category
не найден. Забыл указать новое пространство имен, когда перемещал модели из директории app
в директорию app/Models
. Так что надо исправить файлы в директории database/seeds
и в директории database/factories
.
use Illuminate\Database\Seeder; class CategoryTableSeeder extends Seeder { public function run() { // создать 10 категорий factory(App\Models\Category::class, 10)->create(); } }
use Illuminate\Database\Seeder; class BrandTableSeeder extends Seeder { public function run() { // создать 10 брендов factory(App\Models\Brand::class, 10)->create(); } }
use Illuminate\Database\Seeder; class ProductTableSeeder extends Seeder { public function run() { // создать 30 товаров factory(App\Models\Product::class, 30)->create(); } }
use App\Models\Category; use Illuminate\Support\Str; use Faker\Generator as Faker; $factory->define(Category::class, function (Faker $faker) { /* ... */ });
use App\Models\Brand; use Faker\Generator as Faker; $factory->define(Brand::class, function (Faker $faker) { /* ... */ });
use App\Models\Product; use Faker\Generator as Faker; $factory->define(Product::class, function (Faker $faker) { /* ... */ });
use App\Models\User; use Faker\Generator as Faker; use Illuminate\Support\Str; $factory->define(User::class, function (Faker $faker) { /* ... */ });
Выкладываем изменения на GitHub:
$ git commit -a -m "seeder namespace error" $ git push
На сервере получаем изменения:
$ $ git pull
В третий раз запускаем миграцию:
$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.04 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.04 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.03 seconds) Migrating: 2020_09_28_130327_create_categories_table Migrated: 2020_09_28_130327_create_categories_table (0.04 seconds) Migrating: 2020_09_28_130335_create_brands_table Migrated: 2020_09_28_130335_create_brands_table (0.06 seconds) Migrating: 2020_09_28_130346_create_products_table Migrated: 2020_09_28_130346_create_products_table (0.13 seconds) Migrating: 2020_10_02_120033_create_baskets_table Migrated: 2020_10_02_120033_create_baskets_table (0.03 seconds) Migrating: 2020_10_02_120730_create_basket_product_table Migrated: 2020_10_02_120730_create_basket_product_table (0.13 seconds) Migrating: 2020_10_09_081701_alter_users_table Migrated: 2020_10_09_081701_alter_users_table (0.03 seconds) Migrating: 2020_10_10_105603_create_orders_table Migrated: 2020_10_10_105603_create_orders_table (0.08 seconds) Migrating: 2020_10_10_111729_create_order_items_table Migrated: 2020_10_10_111729_create_order_items_table (0.14 seconds) Migrating: 2020_10_25_090836_alter_orders_table Migrated: 2020_10_25_090836_alter_orders_table (0.04 seconds) Migrating: 2020_10_27_060337_create_pages_table Migrated: 2020_10_27_060337_create_pages_table (0.04 seconds) Migrating: 2020_11_01_064359_create_profiles_table Migrated: 2020_11_01_064359_create_profiles_table (0.07 seconds) Migrating: 2020_11_08_101100_alter_products_table Migrated: 2020_11_08_101100_alter_products_table (0.03 seconds) Seeding: CategoryTableSeeder Seeded: CategoryTableSeeder (0.06 seconds) Таблица категорий загружена данными! Seeding: BrandTableSeeder Seeded: BrandTableSeeder (0.01 seconds) Таблица брендов загружена данными! Seeding: ProductTableSeeder Seeded: ProductTableSeeder (0.05 seconds) Таблица товаров загружена данными! Database seeding completed successfully.
На этот раз все прошло успешно.
5. Создаем символические ссылки
Мы удалили директорию public_html
внутри laravel-7-shop
, которая на хостинге является корнем веб-сервера. Теперь вместо нее создадим символическую ссылку public_html
(см. здесь), которая будет указывать на директорию public
.
$ ln -s ~/laravel-7-shop/public ~/laravel-7-shop/public_html
Кроме того, создаем символическую ссылку public/storage
на директорию storage/app/public
:
$ php artisan storage:link
Исходные коды здесь, демо-сайт здесь.
Post Scriptum
Чтобы на сервере работала отправка почты, нужно создать почтовый ащик в панели управления хостинга, а потом отредактировать файл .env
.
$ nano .env
MAIL_MAILER=smtp MAIL_HOST=smtp.timeweb.ru MAIL_PORT=465 MAIL_USERNAME=laravel-7-shop@tokmakov.msk.ru MAIL_PASSWORD=пароль-почтового-ящика MAIL_ENCRYPTION=ssl MAIL_FROM_ADDRESS=laravel-7-shop@tokmakov.msk.ru MAIL_FROM_NAME="${APP_NAME}"
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 22. Рефакторинг кода, работа над каталогом товаров и корзиной
- Магазин на Laravel 7, часть 21. Добавляем профили и используем их при оформлении заказа
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Класс • Поиск • Практика • Таблица • Форма • Шаблон сайта