Магазин на Yii2, часть 15. Поиск по каталогу товаров, часть первая
05.07.2019
Теги: Web-разработка • Yii2 • ИнтернетМагазин • КаталогТоваров • Поиск • Практика • Фреймворк • ЧПУ
Какой каталог товаров без поиска? Тем более, что и форма в шаблоне предусмотрена. Давайте для начала реализуем самый простой вариант с использованием LIKE. А потом немного усложним — добавим в SQL-запрос расчет релевантности и выполним редирект после отправки формы — чтобы сформировать краcивые URL.
Простой вариант
Начнем с layout-шаблона views/layouts/main.php
, где у нас расположена форма поиска:
<div class="col-sm-4"> <form method="get" action="<?= Url::to(['catalog/search']); ?>" class="pull-right"> <div class="input-group"> <input type="text" name="query" class="form-control" placeholder="Поиск по каталогу"> <div class="input-group-btn"> <button class="btn btn-default" type="submit"> <span class="glyphicon glyphicon-search"></span> </button> </div> </div> </form> </div>
Добавим метод actionSearch()
в контроллер CatalogController
:
<?php namespace app\controllers; use app\models\Category; use app\models\Brand; use app\models\Product; use yii\web\HttpException; use Yii; class CatalogController extends AppController { /* ... */ /** * Результаты поиска по каталогу товаров */ public function actionSearch($query = '', $page = 1) { $page = (int)$page; // получаем результаты поиска с постраничной навигацией list($products, $pages) = (new Product())->getSearchResult($query, $page); // устанавливаем мета-теги для страницы $this->setMetaTags('Поиск по каталогу'); return $this->render( 'search', compact('products', 'pages') ); } }
В класс модели Product
добавим метод getSearchResult()
:
<?php namespace app\models; use Yii; use yii\data\Pagination; use yii\db\ActiveRecord; class Product extends ActiveRecord { /* ... */ /** * Результаты поиска по каталогу товаров */ public function getSearchResult($search, $page) { $search = $this->cleanSearchString($search); if (empty($search)) { return [null, null]; } // пробуем извлечь данные из кеша $key = 'search-'.md5($search).'-page-'.$page; $data = Yii::$app->cache->get($key); if ($data === false) { // данных нет в кеше, получаем их заново $query = self::find()->where(['like', 'name', $search]); // постраничная навигация $pages = new Pagination([ 'totalCount' => $query->count(), 'pageSize' => Yii::$app->params['pageSize'], 'forcePageParam' => false, 'pageSizeParam' => false ]); $products = $query ->offset($pages->offset) ->limit($pages->limit) ->asArray() ->all(); // сохраняем полученные данные в кеше $data = [$products, $pages]; Yii::$app->cache->set($key, $data); } return $data; } /** * Вспомогательная функция, очищает строку поискового запроса с сайта * от всякого мусора */ protected function cleanSearchString($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); return $search; } }
Теперь создадим view-шаблон views/catalog/search.php
:
<?php /* * Страница результатов поиска по каталогу, файл views/catalog/search.php */ use app\components\TreeWidget; use app\components\BrandsWidget; use yii\helpers\Html; use yii\helpers\Url; use yii\widgets\LinkPager; ?> <section> <div class="container"> <div class="row"> <div class="col-sm-3"> <div class="left-sidebar"> <h2>Каталог</h2> <div class="category-products"> <?= TreeWidget::widget(); ?> </div> <h2>Бренды</h2> <div class="brand-products"> <?= BrandsWidget::widget(); ?> </div> </div> </div> <div class="col-sm-9"> <?php if (!empty($products)): ?> <h2>Результаты поиска по каталогу</h2> <div class="row"> <?php foreach ($products as $product): ?> <div class="col-sm-4"> <div class="product-wrapper text-center"> <?= Html::img( '@web/images/products/medium/'.$product['image'], ['alt' => $product['name'], 'class' => 'img-responsive'] ); ?> <h2><?= $product['price']; ?> руб.</h2> <p> <a href="<?= Url::to(['catalog/product', 'id' => $product['id']]); ?>"> <?= Html::encode($product['name']); ?> </a> </p> <a href="#" class="btn btn-warning"> <i class="fa fa-shopping-cart"></i> Добавить в корзину </a> <?php if ($product['new']) { // новинка? echo Html::img( '@web/images/home/new.png', ['alt' => 'Новинка', 'class' => 'new'] ); } if ($product['sale']) { // распродажа? echo Html::img( '@web/images/home/sale.png', ['alt' => 'Распродажа', 'class' => 'sale'] ); } ?> </div> </div> <?php endforeach; ?> </div> <?= LinkPager::widget(['pagination' => $pages]); /* постраничная навигация */ ?> <?php else: ?> <p>По вашему запросу ничего не найдено.</p> <?php endif; ?> </div> </div> </div> </section>
Сейчас URL страниц результатов поиска у нас выглядит так:
http://www.server.com/catalog/search?query=мужская+летняя+одежда http://www.server.com/catalog/search?query=мужская+летняя+одежда&page=2 http://www.server.com/catalog/search?query=мужская+летняя+одежда&page=3 ..........
Добавим правила маршрутизации в файл конфигурации
$config = [ /* ... */ 'components' => [ /* ... */ 'urlManager' => [ /* ... */ 'rules' => [ 'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category', 'catalog/category/<id:\d+>' => 'catalog/category', 'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand', 'catalog/brand/<id:\d+>' => 'catalog/brand', 'catalog/product/<id:\d+>' => 'catalog/product', 'catalog/search/page/<page:\d+>' => 'catalog/search', 'catalog/search' => 'catalog/search', ], ], ], /* ... */ ];
Теперь URL страниц результатов поиска будут выглядеть так:
http://www.server.com/catalog/search?query=мужская+летняя+одежда http://www.server.com/catalog/search/page/2?query=мужская+летняя+одежда http://www.server.com/catalog/search/page/3?query=мужская+летняя+одежда ..........
Сейчас наш код формирует вот такой поисковый запрос:
SELECT * FROM `product` WHERE `name` LIKE '%мужская летняя одежда%'
Чтобы запрос что-то вернул, все три слова должны быть в названии товара. И именно в том порядке, в каком они встречаются в поисковом запросе. Если название товара «Одежда летняя мужская» — то такой товар не попадет в результаты поиска. Давайте это исправим.
<?php namespace app\models; use Yii; use yii\data\Pagination; use yii\db\ActiveRecord; class Product extends ActiveRecord { /* ... */ /** * Результаты поиска по каталогу товаров */ public function getSearchResult($search, $page) { $search = $this->cleanSearchString($search); if (empty($search)) { return [null, null]; } // пробуем извлечь данные из кеша $key = 'search-'.md5($search).'-page-'.$page; $data = Yii::$app->cache->get($key); if ($data === false) { // данных нет в кеше, получаем их заново // разбиваем поисковый запрос на отдельные слова $words = explode(' ', $search); $query = self::find()->where(['like', 'name', $words[0]]); for ($i = 1; $i < count($words); $i++) { $query = $query->andWhere(['like', 'name', $words[$i]]); // $query = $query->orWhere(['like', 'name', $words[$i]]); } // постраничная навигация $pages = new Pagination([/*...*/]); $products = $query ->offset($pages->offset) ->limit($pages->limit) ->asArray() ->all(); // сохраняем полученные данные в кеше $data = [$products, $pages]; Yii::$app->cache->set($key, $data); } return $data; } }
Мы можем сформировать один из двух вариантов запроса — используя andWhere()
или orWhere()
:
SELECT * FROM `product` WHERE (`name` LIKE '%мужская%') AND (`name` LIKE '%летняя%') AND (`name` LIKE '%одежда%')
SELECT * FROM `product` WHERE (`name` LIKE '%мужская%') OR (`name` LIKE '%летняя%') OR (`name` LIKE '%одежда%')
В первом случае, чтобы товар попал в выборку, нужно, чтобы он содержал в названии все три слова. Во втором случае — хотя бы одно слово. Оба варианта — так себе.
Сложный вариант
Давайте добавим в поисковый запрос расчет релевантности. И искать будем не только в названии товара, но и в описании. А результаты поиска отсортируем по убыванию релевантности. В итоге получим такой SQL-запрос:
SELECT *, IF (`name` LIKE '%мужская%', 2, 0) + IF (`content` LIKE '%мужская%', 1, 0) + IF (`name` LIKE '%летняя%', 2, 0) + IF (`content` LIKE '%летняя%', 1, 0) + IF (`name` LIKE '%одежда%', 2, 0) + IF (`content` LIKE '%одежда%', 1, 0) AS `relevance` FROM `product` WHERE `name` LIKE '%мужская%' OR `content` LIKE '%мужская%' OR `name` LIKE '%летняя%' OR `content` LIKE '%летняя%' OR `name` LIKE '%одежда%' OR `content` LIKE '%одежда%' ORDER BY `relevance` DESC
<?php namespace app\models; use Yii; use yii\data\Pagination; use yii\db\ActiveRecord; use yii\db\Query; class Product extends ActiveRecord { /* ... */ /** * Результаты поиска по каталогу товаров */ public function getSearchResult($search, $page) { $search = $this->cleanSearchString($search); if (empty($search)) { return [null, null]; } // пробуем извлечь данные из кеша $key = 'search-'.md5($search).'-page-'.$page; $data = Yii::$app->cache->get($key); if ($data === false) { // данных нет в кеше, получаем их заново // разбиваем поисковый запрос на отдельные слова $words = explode(' ', $search); // рассчитываем релевантность для каждого товара $relevance = "IF (`name` LIKE '%" . $words[0] . "%', 2, 0)"; $relevance .= " + IF (`content` LIKE '%" . $words[0] . "%', 1, 0)"; for ($i = 1; $i < count($words); $i++) { $relevance .= " + IF (`name` LIKE '%" . $words[$i] . "%', 2, 0)"; $relevance .= " + IF (`content` LIKE '%" . $words[$i] . "%', 1, 0)"; } $query = (new Query()) ->select(['*', 'relevance' => $relevance]) ->from('product') ->where(['like', 'name', $words[0]]) ->orWhere(['like', 'content', $words[0]]); for ($i = 1; $i < count($words); $i++) { $query = $query->orWhere(['like', 'name', $words[$i]]); $query = $query->orWhere(['like', 'content', $words[$i]]); } // сортируем разультаты по убыванию релевантности $query = $query->orderBy(['relevance' => SORT_DESC]); // постраничная навигация $pages = new Pagination([ 'totalCount' => $query->count(), 'pageSize' => Yii::$app->params['pageSize'], 'forcePageParam' => false, 'pageSizeParam' => false ]); $products = $query ->offset($pages->offset) ->limit($pages->limit) ->all(); // сохраняем полученные данные в кеше $data = [$products, $pages]; Yii::$app->cache->set($key, $data); } return $data; } }
И последнее, что хотелось бы сделать — изменить URL страниц результатов поиска, чтобы они имели вид:
http://www.server.com/catalog/search/query/мужская+летняя+одежда http://www.server.com/catalog/search/query/мужская+летняя+одежда/page/2 http://www.server.com/catalog/search/query/мужская+летняя+одежда/page/3 ..........
Для этого изменим метод отправки данных с GET
на POST
. И добавим в форму скрытое поле со значением CSRF токена, чтобы не получить ошибку
Bad Request (#400): Не удалось проверить переданные данные.
<div class="col-sm-4"> <form method="post" action="<?= Url::to(['catalog/search']); ?>" class="pull-right"> <?= Html::hiddenInput( Yii::$app->request->csrfParam, Yii::$app->request->csrfToken ); ?> <div class="input-group"> <input type="text" name="query" class="form-control" placeholder="Поиск по каталогу"> <div class="input-group-btn"> <button class="btn btn-default" type="submit"> <span class="glyphicon glyphicon-search"></span> </button> </div> </div> </form> </div>
А в контроллере будем делать редирект на красивый URL результатов поиска, если данные пришли методом POST
:
<?php namespace app\controllers; use app\models\Category; use app\models\Brand; use app\models\Product; use yii\web\HttpException; use Yii; class CatalogController extends AppController { /* ... */ /** * Результаты поиска по каталогу товаров */ public function actionSearch($query = '', $page = 1) { /* * Чтобы получить ЧПУ, выполняем редирект на catalog/search/query/одежда * после отправки поискового запроса из формы методом POST. Если строка * поискового запроса пустая, выполняем редирект на catalog/search. */ if (Yii::$app->request->isPost) { $query = Yii::$app->request->post('query'); if (is_null($query)) { return $this->redirect(['catalog/search']); } $query = trim($query); if (empty($query)) { return $this->redirect(['catalog/search']); } $query = urlencode(Yii::$app->request->post('query')); return $this->redirect(['catalog/search/query/'.$query]); } $page = (int)$page; // получаем результаты поиска с постраничной навигацией list($products, $pages) = (new Product())->getSearchResult($query, $page); // устанавливаем мета-теги для страницы $this->setMetaTags('Поиск по каталогу'); return $this->render( 'search', compact('products', 'pages') ); } }
И изменим правила маршрутизации в файле конфигурации
$config = [ /* ... */ 'components' => [ /* ... */ 'urlManager' => [ /* ... */ 'rules' => [ 'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category', 'catalog/category/<id:\d+>' => 'catalog/category', 'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand', 'catalog/brand/<id:\d+>' => 'catalog/brand', 'catalog/product/<id:\d+>' => 'catalog/product', // правило для 2, 3, 4 страницы результатов поиска 'catalog/search/query/<query:.*?>/page/<page:\d+>' => 'catalog/search', // правило для первой страницы результатов поиска 'catalog/search/query/<query:.*?>' => 'catalog/search', // правило для первой страницы с пустым запросом 'catalog/search' => 'catalog/search', ], ], ], /* ... */ ];
- Магазин на Yii2, часть 16. Поиск по каталогу товаров, часть вторая
- Магазин на Yii2, часть 35. Админка: загрузка картинок для страниц и страница 404
- Магазин на Yii2, часть 34. Показываем меню страниц в публичной части
- Магазин на Yii2, часть 33. Админка: приводим в порядок CRUD-код для страниц
- Магазин на Yii2, часть 32. Админка: удаление категорий и CRUD для страниц
- Магазин на Yii2, часть 31. Админка: загрузка изображений для категорий и брендов
- Магазин на Yii2, часть 30. Админка: WYSIWYG-редактор и изображение для товара
Поиск: Web-разработка • Yii2 • Интернет магазин • Каталог товаров • Поиск • Практика • Фреймворк • ЧПУ • Форма • Form • CSRF • LIKE • Запрос • SEF