Как сделать мультикатегории на MODX с TV-шкой

Всем привет.
Недавно глубоко озадачился этим решением и в результате которого нехитрым (почти) способом можно сделать мультикатегории.
Подходит данное решение для сайтов у которых, наверное не более 5..10к товаров. На больших, думаю, будет тормозить, (не проверял) так что обратная связь приветствуется.

И так, что нам нужно?
  • MODX Revolution
  • TV поле multi
  • Знания sql
  • Знания fenom
  • Бутылочка с пузыриками (выбирайте по вкусу)
1. Создаем TV с нужным именем, у нас это multi.
1.1. В параметрах выбора у нас Тип — список множественный выбор, в возможных значениях, я обычно вписываю @EVAL (вам следует, использовать другой вариант, если хотите чтобы в MODX 3 у вас это дело не слетело (оно не поддерживается в 3-ке):
@EVAL return $modx->runSnippet('pdoResources',array(
'parents'=>4,
'where'=>'{"isfolder:=":1}','depth'=>5,'limit'=>0,'sortby'=>'{"pagetitle":"ASC"}',
'tpl'=>'@INLINE [[+pagetitle]] - ([[#[[+parent]].pagetitle]])==[[+id]]',
'outputSeparator'=>'||','tplWrapper'=>'@INLINE Нет==||[[+output]]'));
В tpl Вы можете сделать как хочется вам у меня маска вывода такая: КАТЕГОРИЯ (родительская категория «на всякий случай»).
В parents — родители из которых брать категории.
where — можно или Isfolder или template использовать — как хотите.
1.2. В параметрах вывода выбираем РАЗДЕЛИТЕЛЬ , (запятая).
1.3. остальное по вкусу. (для каких шаблонов и названия, описания)
В товаре теперь у нас есть TV-шка которая складирует ID дополнительных категорий.
2. настраиваем вывод pdoResource:
2.1. Сложность в том что мультиселект хранит данные в формате: 23||67||288 те с разделителем ||. поэтому выражения с where типа:
a) 'where'=> ["multi:IN" => [99,88] ]
б) 'where'=> ["multi:=" => 88]
в) 'where'=> ["multi:LIKE" => "%88%"]
Не будут корректно работать.
1й вернет товары у которых только одна доп категория, которая равна 88 или 99.
2й вернет товары у которых только одна доп категория, которая равна только 88.
3й вернет все товары, у которых встречаются совпадения с 88. (те 23||67||288 — этот товар попадет в выборку.)
4й вообще даже так записывать — не верно, ведь у нас тут не показана выборка по parent = 88 или 99 а она по умолчанию есть и получиться что нужно указывать корневую Parents = 33, но тогда попадут все товары, значит в where еще нужно дописать parent:IN 88 99. Но и этого мало.

2.2. В общем, нам нужно залайкать в нескольких вариантах multi те.:
LIKE '%|88' и LIKE '88|%' и LIKE '%|88|%' и = '88'
Как видно мы ищем совпадение в конце, совпадение в начале, совпадение в середине и чистое совпадение.
Есть странный глюк в разборе where и если вы захотите написать все в стиле прилежного MODX-сера:
'where'=> ["multi:LIKE" => "%|88","OR:multi:LIKE" => "88|%","OR:multi:LIKE" => "%|88|%","multi:=" => "88"]
Это логично, и читаемо, но массив переменных это зло. И в массиве будет уничтожены (точнее перезаписаны данные) угадайте какие?
Ключ в массиве должен быть уникальным, а значит OR:multi:LIKE у нас останется единственный последний («OR:multi:LIKE» => "%|88|%"). Поэтому нам нужно или поставить пробелы дополнительные чтобы ключ отличался или переписать наш код в более sql ориентированный вариант, что и сделаем ниже (вообще опускаю еще некоторые проблемы которые могут (возможно) возникнуть).

Важно сказать что вывод товаров в категории будет работать только непосредственно в конечных категориях, потому что на входе у нас 1 категория будет активная (ниже объясню почему) структура такая:
Каталог (33) (кат)
— Майки (кат) (55)
— — майка простая артикул 001 (товар) (345) (доп кат 801 и 77)
— — желетка-майка артикул 453 (товар) (987) (доп кат 88)
— Туфли (кат) (88)
— --Туфли с правой ногой (кат) (801)
— --Туфли с левой ногой (кат) (802)
— Брюки (кат) (77)
2.3. Выборка в результате у нас будет такая для вывода ресурсов на фронте:
{set $itzx = $_modx->resource.id} // в зависимости от категории
{'!pdoResources' | snippet : [
    'limit' => 0,
    'hideContainers' => '1',
    'parents' => '33', 
    'includeTVs' => 'multicat', // нужно обязательно добавить, иначе выборка не стработает.
    'processTVs' => 'multicat',
    'where' => [ 
0 => "(`TVmulticat`.`value` LIKE '%|"~$itzx~"' OR `TVmulticat`.`value` LIKE '"~$itzx~"|%' OR `TVmulticat`.`value` LIKE '%|"~$itzx~"|%' OR `TVmulticat`.`value` = '"~$itzx~"' OR parent = "~$itzx~")" ,
1=> [ _ ДРУГИЕ УСЛОВИЯ КАКИЕ_ТО _ ]],
]}
Важный момент обработки массивов. в начале нужно указать 0 => чтобы при обработке это получилось отдельное условие в скобках, потому что после скобок будет идти стандартный AND Например по умолчанию далее идут pulished = 1 deleted = 0, и нам важно чтобы выражение с лайками в скобках воспринималось БД при запросе одним целым, а уже 1 => вы можете добавить свои нужные условия, например template.
а вот строка
OR parent = "~$itzx~"
нужна чтобы товары конкретной категории так же были приравнены к «своим».

Из последнего следует маленький нюанс, что корректно данное решение будет работать только в каталогах, где вывод товаров идет непосредственно в конечных категориях.
Те если мы заходим в категорию Майки (55), мы видим все 2 майки.
Если зайдем в категорию Туфли 88, то мы тут увидим только желетку-майку id987. ну и отдельным сниппетом (блоком) выбор категорий 801 и 802. а уже если зайдем в Туфли с правой ногой id801, то увидим товары которые с правой ногой и дополнительный товар Майка простая, что нам и нужно.

2.4.(UDP 30-07-2023). В комментариях справедливо подсказали более короткое решение через функцию mySql
FIND_IN_SET. Небыло случая проверить, но вот довелось. Меня смутила запись 1=1 AND в начале условия, тк не понятно было что к чему. И вот сегодня после некоторых экспериментов стало ясно. Обработка условия в where идет хитрым образом и если в строке не встречаются корректные данные (нет знака сравнения), то строка вообще вылетает из выборки. (те если я удаляю 1=1, то в логе сниппета — это условие с FIND_IN_SET исчезает чудесным образом). Не долго думая, я поправил это недоразумение, и тк. функция FIND_IN_SET возвращает число — позицию строки в списке строк, разделенных запятыми, то логично, чтобы were сработало корректно нам нужно сравнить результат функции с каким-то числом, а точнее с нулем Запись на феноме (кстати, учите феном и делайте на нем):
[0 => "0 < FIND_IN_SET(" ~ $itzx ~ ", replace(`TVmulticat`.`value`, '||', ','))"]
Таким образом и ошибок меньше и пользы больше. Спасибо @Ivan, что подсказал.

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

PS. теперь можно открыть прохладную шипучку и релакснуть.
Обновлено 30 июля 2023г.
Алексей Смирнов
10 июня 2022, 15:54
modx.pro
3
2 668
+5
Поблагодарить автора Отправить деньги

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

Ivan
10 июня 2022, 17:33
0
Хм. Интересно, а вместо кучи лайков можно сделать так?
"1 = 1 AND FIND_IN_SET(" ~ $itzx ~ ", replace(TVmulticat`.`value`, '||', ',')) OR parent = " ~ $itzx
    Ivan
    10 июня 2022, 17:39
    0
    В теории можно еще выводить товары из дочерних категорий.
    {var $childsCategories  = $_modx->getChildIds($itzx) | join}
    "1 = 1 AND FIND_IN_SET(" ~ $itzx ~ ", replace(TVmulticat`.`value`, '||', ',')) OR parent = " ~ $itzx ~ " OR parent IN (" ~ $childsCategories ~ ")"
    Но тут уже может быть не очень быстро.
      Алексей Смирнов
      10 июня 2022, 17:43
      0
      нужно будет затестить. полезная функция )
        Елизавета
        15 июня 2022, 07:09
        0
        В modx 3 выпилили @EVAL. Как теперь можно запустить конструкцию
        @EVAL $output = $modx->runSnippet('snippet');return '' . $output;
        ?
          Алексей Смирнов
          15 июня 2022, 10:40
          0
          Если у категорий есть свой (а он по идее должен быть) шаблон, то можно попробовать через SELECT:
          @SELECT `pagetitle`,`id` FROM `[[+PREFIX]]site_content` WHERE `published` = 1 AND `deleted` = 0 AND `template` = 15 ORDER BY pagetitle ASC;
            Елизавета
            15 июня 2022, 10:56
            0
            А если нужно учитывать context_key в запросе? Для разных языков нужно брать ресурсы из соответствующего контекста. В таблице site_content есть столбец context_key.
              Алексей Смирнов
              15 июня 2022, 11:07
              0
              о, как. мультиязык на MODX 3? смело.
              Посмотрите в сторону @ CHUNK
              ну и еще есть вариант файл, но файл нужно сгененрировать заранее:
              docs.modx.org/current/ru/building-sites/elements/template-variables/bindings
                Елизавета
                15 июня 2022, 11:28
                0
                Прозвучало как приговор)
                @CHUNK в параметрах TV просто отдает строку. Т.е. если я в чанке вызываю сниппет [[!snippet]], то в результате текстом [[!snippet]], но не результат выполнения.
                  Алексей Смирнов
                  15 июня 2022, 11:56
                  0
                  А вы порпробуйте без воскл. знака сниппет в чанке вызвать.
                  [[Snippet]]
Сергей Карпович
05 марта 2024, 22:02
0
Всем привет, пытаюсь в этот отбор добавить второе условие на проверку заполненности поля players_settings
Но что не получается, так ничего не выводит:
'where' => [ 
        0 => "0 < FIND_IN_SET(" ~ $itzx ~ ", replace(`TVplayers_game`.`value`, '||', ','))" ,
        1 => '{"players_settings:!=":null}',
    ],
Подскажите как правильно прописать условие 1 => '{«players_settings:!=»:null}'?
    Алексей Смирнов
    05 марта 2024, 22:18
    0
    А зачем вам условие players_settings != null?
    Ведь вы по умолчанию проверяете уже в каких категориях состоит ресурс через 1е условие.
    Или опишите задачу.
      Сергей Карпович
      05 марта 2024, 22:39
      0
      Вторым условием нужно убрать ресурсы, у которых поле players_settings не заполнено
        Алексей Смирнов
        06 марта 2024, 08:59
        0
        Зачем второе условие если первым вы уже это делаете.
        Если нет совпадений по первому, то не выведет ничего. Тем более пустое.
          Сергей Карпович
          06 марта 2024, 09:04
          0
          Первым условием мы смотрим «категорию», а вторым условием нужно исключить ресурсы где не заполнено другое TV
            Алексей Смирнов
            06 марта 2024, 10:21
            +1
            А, ясно. Попробуйте так:
            'where' => [ 
                    0 => "0 < FIND_IN_SET(" ~ $itzx ~ ", replace(`TVplayers_game`.`value`, '||', ','))" ,
                    1 => "`TVplayers_settings`.`value` IS NOT null",
                ],
            Если это не сработает, то попробуйте так:
            'where' => [ 
                    0 => "0 < FIND_IN_SET(" ~ $itzx ~ ", replace(`TVplayers_game`.`value`, '||', ',')) AND `TVplayers_settings`.`value` IS NOT null" 
                ],
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
17