[СДЕЛАЙ САМ] Раздел "Избранное" для любых ресурсов на фронте за 8 шагов.

Всех приветствую! Если по каким-то причинам вы не можете купить компонент msFavorites, то предлагаю вашему вниманию свой вариант решения этой задачи. Сразу оговорюсь, решение максимально простое, даже местами примитивное, но рабочее. Так же обращаю ваше внимание, что очистка таблиц не реализована, т.к. не факт, что в этом будет необходимость.

1. Установим необходимые дополнения.
Нам понадобится только Migx и pdoTools.

2. Создадим таблицу для хранения данных
Используя вот эту инструкцию нужно создать пакет «favorites» и таблицу к нему. XML-схему прилагаю
<?xml version="1.0" encoding="UTF-8"?>

<model package="favorites" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" phpdoc-package="" phpdoc-subpackage="" version="1.1">

    <object class="favorites" table="favorites" extends="xPDOSimpleObject">
        <field key="user_id" dbtype="varchar" phptype="string" precision="100" null="false" default=""/>
        <field key="ctx" dbtype="varchar" phptype="string" precision="10" null="false" default=""/>
        <field key="resources" dbtype="text" phptype="string" null="false" default=""/>
        <field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
    </object>

</model>

3. Добавим разметку
{set $favorites = $.session['favorites'] ?: []}
<div class="pos_absolute right-5 top-5 btn js-favorites" data-rid="{$id}" data-action="{($id in list $favorites) ? 'removefromfavorites':'addtofavorites'}">
    <i class="{($id in list $favorites) ? 'icon-heart-fill':'icon-heart'} fs-22-14 color_error"></i>
</div>
В разметке следует обратить внимание на класс «js-favorites» — обозначает блок, клик по которому будет отправлять запрос на сервер для добавления/удаления ресурса в Избранное. У этого блока должны быть два обязательных data-атрибута «rid» (id ресурса) и «action» (ключ действия). И классы иконки "'icon-heart-fill" — показывается если ресурс есть в избранном, и "'icon-heart" — показывается, если ресурса нет в избранном.

4. Напишем JS для отправки запроса
document.addEventListener('DOMContentLoaded', (e) => {
    document.addEventListener('click', async (e) => {
        // добавление в избранное
        if (e.target.classList.contains('js-favorites') || e.target.closest('.js-favorites')) {
            const target = e.target.classList.contains('js-favorites') ? e.target : e.target.closest('.js-favorites');
            const params = new FormData();
            const icon = target.querySelector('i');
            params.append('action', target.dataset.action);
            params.append('rid', target.dataset.rid);
            let response = await fetch('assets/project_files/action.php', {
                method: 'POST',
                headers: {'X-Requested-With': 'XMLHttpRequest'},
                body: params
            });
            if (response.ok) {
                const result = await response.json();
                if (result.success) {
                    if (icon) {
                        if (result.data.action === 'removefromfavorites') {
                            icon.classList.remove('icon-heart');
                            icon.classList.add('icon-heart-fill');
                        }
                        if (result.data.action === 'addtofavorites') {
                            icon.classList.remove('icon-heart-fill');
                            icon.classList.add('icon-heart');
                        }
                    }
                    target.dataset.action = result.data.action;
                    miniShop2.Message.success(result.message);
                } else {
                    miniShop2.Message.error(result.message);
                }
            }
        }
    });
});
Тут стоит обратить внимание на то, что уведомления показываются с помощью miniShop2.Message, если miniShop2 у вас на сайте не установлен, удалите эти строки и используйте свои методы вывода уведомлений.

5. Напишем приемник запроса на PHP
Написанный выше JS шлёт запрос на «assets/project_files/action.php». Создадим этот файл и разместим в нём следующий код:
<?php

define('MODX_API_MODE', true);
require_once dirname(dirname(dirname(__FILE__))) . '/index.php';

$modx->getService('error', 'error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_ERROR);

if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {
    return;
}

if (empty($_POST['action'])) {
    return;
}

$res = ['success' => 1, 'message' => '', 'data' => []];
$action = $_POST['action'];
if(!$_SESSION['session_id']){
    $_SESSION['session_id'] = session_id();
}
switch ($action) {
    case 'addtofavorites':
        $res['success'] = false;
        if ($modx->addPackage('favorites', MODX_CORE_PATH . 'components/favorites/model/')) {
            $user_id = $modx->user->get('id') ?: session_id();
            if (!$favorites = $modx->getObject('favorites', ['user_id' => $user_id, 'ctx' => $modx->context->get('key')])) {
                $favorites = $modx->newObject('favorites');
                $data = [
                    'user_id' => $user_id,
                    'createdon' => time(),
                    'resources' => (int)$_POST['rid'],
                    'ctx' => $modx->context->get('key')
                ];
            } else {
                $data = $favorites->toArray();
                $resources = explode(',', $data['resources']);
                $resources[] = (int)$_POST['rid'];
                $data['resources'] = implode(',',array_unique($resources));
            }

            $favorites->fromArray($data);
            $favorites->save();
            $res['success'] = true;
            $res['message'] = 'Товар добавлен в Избранное';
            $res['data'] = ['id' => (int)$_POST['rid'], 'action' => 'removefromfavorites'];
        }
        break;
    case 'removefromfavorites':
        $res['success'] = false;
        if ($modx->addPackage('favorites', MODX_CORE_PATH . 'components/favorites/model/')) {
            $user_id = $modx->user->get('id') ?: session_id();
            if ($favorites = $modx->getObject('favorites', ['user_id' => $user_id, 'ctx' => $modx->context->get('key')])) {
                $data = $favorites->toArray();
                $resources = explode(',', $data['resources']);
                $key = array_search((int)$_POST['rid'], $resources);
                if($key !== false){
                    unset($resources[$key]);
                }

                if(!empty($resources)){
                    $data['resources'] = implode(',', $resources);
                    $favorites->fromArray($data);
                    $favorites->save();
                }else{
                    $favorites->remove();
                }

                $res['success'] = true;
                $res['message'] = 'Товар удален из Избранного';
                $res['data'] =['id' => (int)$_POST['rid'], 'action' => 'addtofavorites'];
            }
        }
        break;
}
if (!empty($res)) {
    die(json_encode($res));
}

6. Создадим плагин на OnWebLogin
Этот плагин позволит сохранять товары в Избранном после авторизации пользователя на сайте.
<?php

switch ($modx->event->name) {
    case 'OnWebLogin':
        $session_id = $_SESSION['session_id'];
        if ($modx->addPackage('favorites', MODX_CORE_PATH . 'components/favorites/model/')) {
            if ($favoritesBySession = $modx->getObject('favorites', ['user_id' => $session_id, 'ctx' => $loginContext])) {
                if ($favoritesByUserId = $modx->getObject('favorites', ['user_id' => $user->get('id'), 'ctx' => $modx->context->get('key')])) {
                    $dataBySession = $favoritesBySession->toArray();
                    $dataByUserId = $favoritesByUserId->toArray();
                    $resourcesBySession = explode(',', $dataBySession['resources']);
                    $resourcesByUserId = explode(',', $dataByUserId['resources']);
                    $resources = array_unique(array_merge($resourcesByUserId, $resourcesBySession));
                    $favoritesBySession->set('resources', implode(',', $resources));
                }
                $favoritesBySession->set('user_id', $user->get('id'));
                $favoritesBySession->save();
            }
        }
        break;
}
Обратите внимание, я передаю в плагин через сессию идентификатор сессии, это связано с тем, что у меня на сайте авторизация происходит не через процессор, а с помощью метода addSessionContext(), который меняет id сессии, а нужен тот id, который был до авторизации. Возможно, при авторизации через процессор, такого не происходит, но я не проверял.

7. Создадим сниппет getFavorites для получения списка Избранного
<?php
$output = '';
if ($modx->addPackage('favorites', MODX_CORE_PATH . 'components/favorites/model/')) {
    $user_id = $modx->user->get('id') ?: session_id();
    if ($favorites = $modx->getObject('favorites', ['user_id' => $user_id, 'ctx' => $modx->context->get('key')])) {
        $data = $favorites->toArray();
        if($returnArray){
            $output = explode(',', $data['resources']);
        }else{
            $output = $data['resources'];
        }
    }
}

if($toPls){
    $_SESSION['favorites'] = $output;
}else{
    return $output;
}
Вызывать его нужно один раз, где-то в шапке, так как результаты работы он записывает в сессии.
{'!getFavorites' | snippet: ['toPls' => 1, 'returnArray' => 1]}
Если убрать параметр «toPls» результат не будет записан в сессию и результат работы нужно сохранить в переменную. Чтобы вернуть строку, нужно убрать «returnArray». Однако я возвращаю массив, поскольку по нему удобнее делать проверку (см. п.3).

8. Выведем список на странице
<section id="favorites_16878692072705" class="py-40-20 section-favorites">
    <div class="container" id="pdopage">

        {set $resources = $.session['favorites'] | join: ','}
        {if $resources}
            <div class="rows grid_sq_md-4-1 grid_sq-2-1 gap_col-20-10 gap_row-40-20 pb-60-20">
                {'!pdoPage' | snippet: [
                'parents' => '0',
                'element' => 'msProducts',
                'resources' => $resources,
                'ajaxMode' => 'button',
                'includeTVs' => 'img',
                'tvPrefix' => '',
                'tpl' => '@FILE chunks/msproducts/item.tpl',
                'limit' => '4',
                'ajaxTplMore' => '@INLINE  <div class="d_flex ai_center jc_sm_end jc_center"><button class="btn btn-more m-0 fs-14 link">Показать ещё</button></div>',
                ]}</div>
            {'page.nav' | placeholder}
        {else}
            <h3 class="fs-26-20 ff_proxima-nova-bold py-30-20 bg_light radius_pill ta_center grid__col-1-13">Нет товаров в Избранном</h3>
        {/if}
    </div>
</section>
Разметка может быть любая, основное это вызов pdoPage.

Всем спасибо за внимание.

О том как поблагодарить автора и поддержать сообщество можно узнать тут
Артур Шевченко
27 июня 2023, 22:31
modx.pro
5
1 022
+5
Поблагодарить автора Отправить деньги

Комментарии: 6

Алексей Соин
27 июня 2023, 23:21
0
это дополнение разве не тоже самое уже реализует?
deleted
28 июня 2023, 09:35
+1
Что-то как-то сложно) Я просто сохраняю в куки id ресурсов и потом вывожу их через pdoResources / msProducts
    Артур Шевченко
    28 июня 2023, 09:39
    0
    Куки ненадёжно, пользователь может их очистить.
      Николай Савин
      28 июня 2023, 10:06
      0
      Сессии так-то тоже не везде используются. Перезаписываются при каждой авторизации
    Николай Савин
    28 июня 2023, 10:11
    +1
    Обратите внимание, я передаю в плагин через сессию идентификатор сессии, это связано с тем, что у меня на сайте авторизация происходит не через процессор, а с помощью метода addSessionContext(), который меняет id сессии, а нужен тот id, который был до авторизации. Возможно, при авторизации через процессор, такого не происходит, но я не проверял.
    Процессор использует ровно тот же метод addSessionContext, добавляя еще проверку прав, запуск событий и т.п. Так что все идентично по факту. И да — addSessionContext меняет id сессии. Способа бороться с этим нет.
    Я для себя делаю так

    $_SESSION['prev_session_id'] = session_id();
    $user->addSessionContext();
    И далее запрос в базу избранного можно строить используя текущий обновленный session_id и одновременно старый из сессии.
      Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
      6