[СДЕЛАЙ САМ] Поиск на сайте по-быстрому
ВАЖНО!!!
Я знаю, что есть два отличных компонента для организации поиска по сайту это SimpleSearch, который по-моему не работает с чанками на fenom, и mSearch2, который платный и большой. В связи с этим я решил написать свой вариант поиска — максимально простой и понятный, да неуниверсальный, но позволяющий по нескольким ключевым словам найти нужный ресурс. По сути это форма. которая через ajax отправляется на сервер, где формируется sql-запрос в БД и результат возвращается на фронт. Итак перейдём к коду.
ФРОНТ
Тут использована сетка bootstrap5, иконочный шрифт FontAwesome
Если решите менять вёрстку обязательно нужно сохранить все тэги input, все атрибуты id, все классы начинающиеся с js.
JAVASCRIPT
Я использовал чистый JavaScript для отправки ajax.
PHP
На сервере я использовал два сниппета ajaxReceiver для принятия запроса и searchResources для поиска.
Сниппет ajaxReceiver
Сниппет searchResources
АЛЬТЕРНАТИВНЫЙ ВАРИАНТ
Пожалуй, этот способ ещё проще, но требует чтобы были установлены AjaxForm и FormIt.
ФРОНТ
Сначала вызываем AjaxForm
JAVASCRIPT
Отправку запроса делает AjaxForm, на остается только принять ответ и вывести данные.
PHP
В этом случае нам нужен только сниппет searchResources с небольшими изменениями
Данное решение является учебным и рекомендуется к использованию только на собственных проектах, с целью освоения работы с api modx. Для изучения лучших практик, анализируйте код дополнений с сайта https://modstore.pro/
Я знаю, что есть два отличных компонента для организации поиска по сайту это SimpleSearch, который по-моему не работает с чанками на fenom, и mSearch2, который платный и большой. В связи с этим я решил написать свой вариант поиска — максимально простой и понятный, да неуниверсальный, но позволяющий по нескольким ключевым словам найти нужный ресурс. По сути это форма. которая через ajax отправляется на сервер, где формируется sql-запрос в БД и результат возвращается на фронт. Итак перейдём к коду.
ФРОНТ
Тут использована сетка bootstrap5, иконочный шрифт FontAwesome
Если решите менять вёрстку обязательно нужно сохранить все тэги input, все атрибуты id, все классы начинающиеся с js.
<div class="container">
<form class="row mt-lg-5 jsSearchForm">
<input type="hidden" name="action" value="search">
<input type="hidden" name="tpl" value="@FILE chunks/blocks/service-item.html">
<div class="col-9">
<label class="label-text par">
<input type="text" name="query" class="input" placeholder="Например, купить в 1 клик">
</label>
</div>
<div class="col-3">
<button class="btn btn-orange btn-cart" type="submit">
<i class="fa fa-search"></i>
</button>
</div>
<div class="col-12">
<span class="text-red par jsError"><!-- сюда будем вставлять текст ошибки --></span>
</div>
</form>
</div>
<div class="container mt-lg-4 mt-3" id="productList">
<!-- сюда будем вставлять результаты поиска -->
</div>JAVASCRIPT
Я использовал чистый JavaScript для отправки ajax.
// вешаем обработчик на форму поиска
document.querySelector('.jsSearchForm').addEventListener('submit', function (e) {
e.preventDefault();
let formData = new FormData(e.target.closest('form'));
sendAjax(formData);
});
// отправляем ajax
function sendAjax(params) {
// Создаем экземпляр класса XMLHttpRequest
const request = new XMLHttpRequest();
// Указываем путь до файла на сервере, который будет обрабатывать наш запрос
const url = document.location.href;
// Указываем что соединение у нас будет POST
request.open('POST', url);
request.responseType = 'json'; // ответ должен быть в формате json
// обрабатываем ответ сервера
request.addEventListener("readystatechange", () => {
if (request.readyState === 4 && request.status === 200) {
//console.log(request.response);
// показываем результаты поиска
if(document.getElementById('productList') && request.response.products){
document.getElementById('productList').innerHTML = request.response.products;
}
// выводим ошибки если есть
if(document.querySelector('.jsError') && !request.response.success){
document.querySelector('.jsError').innerHTML = request.response.message;
}
// убираем ранее выведенные ошибки
if(document.querySelector('.jsError') && request.response.success){
document.querySelector('.jsError').innerHTML = '';
}
}
});
// Вот здесь мы и передаем строку с данными, которую формировали выше. И собственно выполняем запрос.
request.send(params);
}PHP
На сервере я использовал два сниппета ajaxReceiver для принятия запроса и searchResources для поиска.
Сниппет ajaxReceiver
<?php
// Откликаться будет ТОЛЬКО на ajax запросы методом POST
if($_SERVER['REQUEST_METHOD'] != 'POST' ) {return;}
// Сниппет будет обрабатывать не один вид запросов, поэтому работать будем по запрашиваемому действию
// Если в массиве POST нет действия - выход
if (empty($_POST['action'])) {return;}
// А если есть - работаем
$res = '';
$action = $_POST['action'];
switch ($action) {
case 'search':
$res = $modx->runSnippet('searchResources');
break;
// А вот сюда потом добавлять новые методы prodFastView
}
// Если у нас есть, что отдать на запрос - отдаем и прерываем работу парсера MODX
if (!empty($res)) {
die($res);
}Сниппет searchResources
<?php
// задаем переменные
$res = [];
$query = strip_tags($_POST['query']);
$tpl = $_POST['tpl'];
if($_POST['minQuery'] && is_numeric($_POST['minQuery'])){
$minQuery = (int)$_POST['minQuery'];
}
else{
$minQuery = 3;
}
/*
Поля для поиска совпадений можно передать с фронта, но стоит помнить,
что запрос делается к модели класса msProduct,
а значит и поля можно использовать только из таблицы ms2_products и site_content.
Если нужны другие поля, то нужно либо менять класс либо использовать JOIN.
*/
if(isset($_POST['fields'])){
$fields = explode(',',$_POST['fields']); // строка вида - 'pagetitle,longtitle,menutitle'
}
else{
$fields = ['pagetitle','longtitle','menutitle']; // либо массив
}
// проверяем возможность дальнейшей работы
if(!$query){return false;}
if(mb_strlen(str_replace(' ', '', $query)) < $minQuery){
$res = ['success' => false, 'message' => 'Количество символов в запросе должно быть больше ' . $minQuery];
return json_encode($res);
}
// формируем запрос в БД
$where = [];
$dbQuery = $modx->newQuery('msProduct');
foreach($fields as $k => $field){
if($k == 0){
$where[$field.':LIKE'] = '%'.str_replace(' ', '%',$query).'%';
}
else{
$where['OR:'.$field.':LIKE'] = '%'.str_replace(' ', '%',$query).'%';
}
}
$dbQuery->where( $where );
$dbQuery->select(['id']);
// выполняем запрос в БД
if ($dbQuery->prepare() && $dbQuery->stmt->execute()) {
$ids = $dbQuery->stmt->fetchAll(PDO::FETCH_COLUMN);
}
/*
Дальше возможны варианты, мне требовался поиск по товарам,
поэтому я просто сформировал массив id-шников и вызвал сниппет msProducts,
а в целом можно писать любую логику.
*/
// если что-то найдено
if(count($ids)){
$pdoTools = $modx->getService('pdoTools');
$res['products'] = $pdoTools->runSnippet('msProducts', array(
'parents' => 9,
'resources'=> implode(',', $ids),
'limit' => 0,
'sortby' => ['menuindex' => 'ASC'],
'tpl' => $tpl
));
$res['success'] = true;
}
// если ничего нет
else{
$res = ['success' => false, 'message' => 'По запросу <b class="text-orange">«'.$query.'»</b> ничего не найдено. Попробуйте убрать окончание слова.'];
}
return json_encode($res);АЛЬТЕРНАТИВНЫЙ ВАРИАНТ
Пожалуй, этот способ ещё проще, но требует чтобы были установлены AjaxForm и FormIt.
ФРОНТ
Сначала вызываем AjaxForm
<div class="container">
{'!AjaxForm' | snippet: [
'snippet' => 'searchResources',
'tpl' => '@FILE chunks/blocks/service-item.html',
'form' => '@FILE chunks/forms/searchForm.html',
'fieldsForSearch' => 'pagetitle,longtitle,menutitle',
'validate' => 'query:required:minLength=^3^',
'placeholderPrefix' =>'find.'
]}
</div>
<div class="container mt-lg-4 mt-3" id="productList">
<!-- сюда будем вставлять результаты поиска -->
</div>Чанк с формой chunks/forms/searchForm.html выглядит так<form class="row mt-lg-5">
<div class="col-9">
<label class="label-text par">
<input type="text" name="query" class="input" placeholder="Например, купить в 1 клик">
</label>
</div>
<div class="col-3">
<button class="btn btn-orange btn-cart" type="submit">
<i class="fa fa-search"></i>
</button>
</div>
<div class="col-12 error_query par"><!-- сюда будем вставлять текст ошибки --></div>
</form>JAVASCRIPT
Отправку запроса делает AjaxForm, на остается только принять ответ и вывести данные.
$(document).on('af_complete', function(event, response){
if (!response.success) {
if (typeof response.data.error !== "string") {
for (let key in response.data.error) {
let keys = key.split('.');
if ($(response.form).find('.error_' + keys[2]).length) {
$(response.form).find('.error_' + keys[2]).html(response.data.error[key]); // выводим текст ошибки
}
}
}
}
else{
if($('#productList')){
$('#productList').html(response.data.products); // выводим результаты поиска
}
}
});PHP
В этом случае нам нужен только сниппет searchResources с небольшими изменениями
<?php
//проверяем форму
$result = $modx->runSnippet('FormIt', $scriptProperties);
foreach($modx->placeholders as $key => $ph){
if(strpos($key, 'find.error.') === 0){
$placeholders[$key] = $ph;
}
}
//если нет ошибок работаем дальше
if(!count($placeholders)){
// задаем переменные
$res = [];
$query = strip_tags($_POST['query']);
$tpl = $scriptProperties['tpl'];
$fields = explode(',',$scriptProperties['fieldsForSearch']) ?: ['pagetitle','longtitle','menutitle'];
// проверяем, чтобы поля для поиска были массивом
if(is_array($fields)){
// формируем запрос в БД
$where = [];
$dbQuery = $modx->newQuery('msProduct');
foreach($fields as $k => $field){
if($k == 0){
$where[$field.':LIKE'] = '%'.str_replace(' ', '%',$query).'%';
}
else{
$where['OR:'.$field.':LIKE'] = '%'.str_replace(' ', '%',$query).'%';
}
}
$dbQuery->where( $where );
$dbQuery->select(['id']);
// выполняем запрос в БД
if ($dbQuery->prepare() && $dbQuery->stmt->execute()) {
$ids = $dbQuery->stmt->fetchAll(PDO::FETCH_COLUMN);
}
// если что-то найдено
if(count($ids)){
$pdoTools = $modx->getService('pdoTools');
$products = $pdoTools->runSnippet('msProducts', array(
'parents' => 9,
'resources'=> implode(',', $ids),
'limit' => 0,
'sortby' => ['menuindex' => 'ASC'],
'tpl' => $tpl
));
$res['success'] = true;
return $AjaxForm->success('Результатов '.count($ids).' шт.', array('products' => $products));
}
// если ничего нет
else{
return $AjaxForm->success('По запросу «'.$query.'» ничего не найдено. Попробуйте убрать окончание слова.');
}
}else{
return $AjaxForm->error('Неверный формат параметра $fields');
}
}
//если есть ошибки
else{
return $AjaxForm->error('В форме содержатся ошибки', array('error' => $placeholders));
} Комментарии: 31
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
Объекты тут вообще не нужны, гораздо правильнее заменить это на
$query было бы неплохо хотя бы поверхностно обработать, т.к. он дальше напрямую передается в запрос.
Можно добавить (int).
С одной стороны да, а с другой, изначально я не планировал использовать msProducts, поэтому и получал объекты.
Прям согласен.
Тогда уже так
Если у тебя будет 30к товаров, то getIterator сожрет всю оперативку даже не дойдя до половины.
Конечно, защищает от любых sql-инъекций.
достаточно просто
А если $_REQUEST['minQuery'] будет равна 'три', тут
будет ошибка, нет?
Тем более, в твоем примере просто проверяется наличие '@FILE' в строке, а не на первой позиции. Это я к тому, что накосячить с этим гораздо проще, чем кажется.
То она просто скастуется до 0 через (int), а затем будет взято 3. В общем, в этой переменной всегда будет число и ошибки не будет. Максимум — условие не пройдет, если передать отрицательный minQuery.
event — это submit?
e.target — если сабмит, то это и есть форма.
тыц
Во-вторых, зачем мне поиск по TV, чанкам, сниппетам и пользователям, это избыточный функционал для моей задачи.
В-третьих, я не учел что такой процессор существует.
В-четвёртых, для саморазвития полезно изобрести велосипед другой, в конце концов я никого на нём ездить не заставляю:-)
searchResources юзает getCollection, ровно как и остальные методы для поиска, тут можно передать привет оперативке.
process тоже переопределять нужно.
И что там в итоге остается, проверка прав?
Зачем? Во первых вызывая сниппет — ты проходишь полный цикл инициализации MODX, тебе нужно отдать очень простые результаты зачем тебе полностью инициализировать modx? Дикая нагрузка на сервер, очень медленная скорость. Также ты вызываешь еще один сниппет из этого сниппета, это прям ну вообще плохо
Эм… Зачем вызывать сниппет msProducts? Почему бы не использовать pdoFetch например и не задействовать парсер modx?
И это лишь малая часть, тут вообще ни строчки правильного кода по сути нет…
Короче без обид, Артур, учиться программировать не плохо, что мы видим из твоих статей, но пожалуйста, называй свои статьи не «Как сделать что то» а «Подскажите что я делаю не так» ну или хотя бы «Мой способ реализации бла-бла». Твой абсолютный говнокод же будут потом брать новички (что мы видим из количества добавленных в избранное), а учитывая как мало материалов по modx он еще и в гугл может попасть…
Что значит «полный цикл инициализации MODX» и что в этом страшного, если на странице зачастую вызывается несколько сниппетов?
Я понимаю, что это, скажем так, неоптимальное решение, но другой вариант это
А как без использования парсера? Мне же html нужен, а не сырые данные, или JS'ом вставлять?
А что до названия, ну «Сделай Сам» или «Мой способ реализации бла-бла» так по-моему без разницы, на код это не влияет)))
1) Ну во первых надо было свой сервис сделать у него хандлер и пр, если уж так не хочется использовать процессоры
2) Изучи docs.modx.pro/komponentyi/pdotools/parser, тут не очень подробно, но эксперементируя — понятно
На код не влияет, а на восприятие людьми, еще менее знающими чем ты — еще как. Начинающие не знают что хорошо, а что плохо и запросто будут использовать твой код как пример хорошей практики и возможно на нем даже учится, вон, видишь, даже 4 человека в избранное твою статью добавили, значит планируют использовать твой код хотя бы в теории
Где прервать? И как с помощью die()?
Согласен. Предупреждение написал.
github.com/pavel-one/modxCustomAuth/tree/master/assets/components/customAuth
Хорошей и верной практикой — да, единственной — нет, репозиторий который выше кинул поизучай, вполне себе рабочая практика написания своих велосипедов без знания ООП и принципов работы modx, собственно именно при таких же знаниях я это и писал