MODX - msFilter2

Введние


mFilter2 - это очень крутой фильтр для вывода чего угодно, как пишет его автор. Является частью mSearch2.

Как всегда это пишу больше для себя в качестве конспекта. Здесь цель  - понять логику фильтрации при изменении параметров фильтра уже загруженной страницы. И интересует как программиста, с погружением в исходники и структуры.

 

Структура вывода


Основной чанк (парметр:tplOuter) выводится прямо на месте вызова сниппета. Поэтому многие туда практичеки весь body пихают. Хорошо бы конечно, чтоб можно было в контейнер. Ну пусть. И должен в себе содержать примерно такую структуру.

 

Классы фильтров(php)


Наименование Класс, скрипт Системная настройка, пример
классы фильтров /core/components/msearch2/model/msearch2/filters.class.php mse2_filters_handler_class
расширения фильтров /core/components/msearch2/custom/filters/имя_фильтра.class.php <?php class myCustomFilter extends mse2FiltersHandler { // Здесь можно переопределить методы родительского класса, или создать собственные }

Методы фильтрации


Может сбить с толку кого угодно. На самом деле фильтруют только методы filter.....

Методы build..... - для вывода самих фильтров. Т.е. полей ввода, чекбоксов, слайдеров и прочих. 

Методы get.... - для группировки исходного массива id ресурсов по кодовым именам таблиц, именам полей и значениям полей. Это - крутейшая идея. Вот с этими массивом ($this->filters) и работают методы build.... и filter..... . Этот массив не меняется при смене параметров фильтрации, так как зависит только от исходного массива и от назначенных фильтров в параметрах вызова mFilter2  хотя и генерится всякий раз заново. Но это не точно).

Наименование Пример фильтра Пример метода
getИмяValues resource|parent:categories getResourceValues
buildИмяFiltr ms|price:number buildNumberFilter
filterИмя ms|price:number filterNumber

Массивы, возращаемые методами.


1. get....Values    - массив значений

Array (
    [ИмяПоля 1] => array(
        [Значение1] => array(
            [0] => id подходящего ресурса
            [1] => id подходящего ресурса
            [2] => id подходящего ресурса
        ),
        ........................
        ........................
        ........................

    ),
    ..................
    ..................
    ..................

)

2.build....Filter - массив для вывода фильтров

Array (
    [ИмяФильтра] => Array (
        [title] => ИмяФильтра
        [value] => значение позиции фильтра
        [type] => необязательное поле с типом фильтра
        [resources] => Array (
            [0] => id подходящего ресурса
            [1] => id подходящего ресурса
            [2] => id подходящего ресурса
        )
    )
)

3.filter....- непосредственно фильтр.

на входе получает 3 массива из первых методов

  • Массив с запрошенными значениями (напр. parent=42).
  • Массив имеющихся значений, где ключами являются значения фильтров, а значениями - подходящие ресурсы. Этот массив - результат метода get....Values
  • Текущий массив результатов. Что осталось от предыдущих фильтров.

Например при фильтре по предку: parent=71 -

 

Array (
    [0] => 71
) - значения(запрошенные) parent

Array (
    [71] => Array (
        [0] => 72
        [1] => 73
        [2] => 74
    )
) - то, что есть сейчас

Array (
    [0] => 72
    [1] => 73
    [2] => 74
    [3] => 75
    [4] => 76
) - что осталось от пред. фильтров

Далее массив 2 сравнивается по ключам со значениями массива 1, а по значениям - со значениями массива 3.

Кодовые имена таблиц


Кодовое имя Имя таблицы реальное(в модели!!!) Поля(не все. читсто для ориентации)
resource modResource pagetitle, longtitle
tv modTemplateVar могут быть любые
ms msProductData price, article, weight
msoption msProductOption size, color, tags
msvendor msVendor title, country, phone

Встроенные фильтры


Название Что делает
default Строит фильтр по умолчанию, состоящий их чекбоксов
number Строит фильтр для чисел, который можно вывести в виде слайдера
vendors Можно применять только к полю vendor ms|vendor:vendors.
boolean Для булевых значений. Нужен для того, чтобы вместо 0 и 1 вы видели
parents Выбираются и показываются два родителя ресурса. Можно применять только к полю parent resource|parent:parents.
categories Выбирается и показывается один родитель ресурса. Можно применять только к полю parent resource|parent:categories
grandparents Выбираются и показываются второй родитель ресурса. Можно применять только к полю parent resource|parent:grandparents.
fullname Полные имена пользователей. Только к id юзера, например resource|createdby:fullname.
year Год, например resource|createdon:year.
month К полям с датой.Выводит месяц прописью, подставляя его название из словаря компонента.
day К полям с датой и выводит день.

Виджеты


JS назначается в настройке mse2_frontend_js,  по умолчанию - /assets/components/msearch2/js/web/default.js ([ [+jsUrl]]web/default.js)

Но пока не до них).

Обработка фильтра


Предполагается, что массив ресурсов первоначально уже сформирован. Нас интересует сам механизм фильтрации.

При смене, например цены, проиисходит следующее:

  • виджет отправляет ajax на "/assets/components/msearch2/action.php", POST-параметры: ms|price=82276,765538; action=filter; pageId=620 и key=131321... Далее а action.php для action=filter
    • объект $mSearch2. 'core/components/msearch2/model/msearch2/msearch2.class.php
    • установка свойств пагинации и сортировки.
    • $matched = $mSearch2->Filter($ids, $_REQUEST); - то, что надо нам
      $ids - исходный массив ресурсов, не меняется при изменении фильтров. из $_SESSION['mSearch2'][$key]['paginatorProperties']['resources']   , $key=$_REQUEST['key']
      $_REQUEST - примерно такой
      $request=Array
      (
          [ms|price] => 2070,20981
          [ms|vendor] => 5,7,12
          [resource|parent-grandparents] => 617,620
          [resource|parent-categories] => 645,654
          [q] => rolstavni2.html
          [limit] => 12
      )
      
      • $this->getFilters($ids, $build=true); - вернет массив фильров. Если второй параметр true - создаст массив build. тут вызывается с false.
        $ids - исходный массив, не меняется при изменении фильтров
        • $this->loadHandler(); Загрузка класса содержащего методы get, buid, filter.
          • require_once 'filters.class.php'; - тут все встроенные классы фильтров. 
            • класс  mse2FiltersHandler.
              • get...Values - методы
              • build...Filter - методы. Для выводов самих фильтров слева.
              • filter.... - методы
          • если не находит встроенных фильтров -> $this->loadCustomClasses('filters'); - загружает пользовательские
          • $this->filtersHandler = new $filters_class($this, $this->config); $filters_class=mse2FiltersHandler; можно свой сделать.
        • обрабатывает параметр &filters=«таблица|поле:метод». напр.: resource|parent:parents
        • попутно генерится массив представлений самих фильтров -> $built[$table . $this->config['filter_delimeter'] . $name] = $filter;  $build["resource|parent"]="category"
        • $method = 'get' . ucfirst($table) . 'Values'; - для каждого фильтра в параметрах вычисляет метод get...Values
        • method_exists($this->filtersHandler, $method) - проверка наличия метода 
          • вызов $fields=$this->filtersHandler->get[tableName]Values($fields,$ids), где $fields - ссылка на $filters[$table][$fields], т.е. ему и присваивается
            • выборка по $ids все в массив ->  $filters["поле"]["значение поля"][$id] = $id;
            • return $filters - это на самом деле $fields. Алгоритм настолько замороченный, что легко запутаться. Тут ничего не выбирается на самом деле, а просто $idx раскладывается по полям и их значениям.
        • $this->filters = $filters;   - массив id, группами по полям и значениям.  $filters["resource"]["title"]["Осень в Соколе"][34] = 34;
          $this->methods = $built; - массив фильтров $built["resource|title"] = "default";  

        • замена в именах фильтров "." на "_"
        • if (!$build) return $this->filters;
        • if($build)  
          • $built = $this->methods;
          • цикл по $this->filters
            • $filter= $built[$table|$field], если не существует, то $filter="default"
            • $method = 'build'.ucfirst($filter).'Filter'; if $filter="default" -> $method = 'buildTVsFilter', $method = 'buildOptionsFilter'; при $table=''tv" || "msoption"
            • если есть, то $prepared[$table.'|'.$field] =$this->filtersHandler->$method($values,$fieldName),  $values[$v]=[123,2324,4546,....] - т.е. ids группами по значениям поля
            • далее каждому $built[$table.'|'.$field]=$prepared[$table.'|'.$field], т.е. результат выполнения $this->filtersHandler->$method($values,$fieldName)
            • Итоговый массив $built=
              Array ( [ИмяФильтра] => Array ( [title] => ИмяФильтра [value] => значение позиции фильтра [type] => необязательное поле с типом фильтра [resources] => Array ( [0] => id подходящего ресурса [1] => id подходящего ресурса [2] => id подходящего ресурса ) )
              • return $built
      • обработка $_REQUEST. Реально до этого момента - ничего не менялось. Вот тут и будет фильтрация!
        • отфильтровываются только фильтры
        • в итоге после подработки - $request=Array
          (
              [ms|price] => 1334,20981
              [ms|vendor] => 17
              [resource|parent-grandparents] => 617
              [resource|parent-categories] => 628 
          )
        • к этому моменту $this->filters=
          Array
          (
              [ms] => Array
                  (
                      [price] => Array
                          (
                              [0.00] => Array
                                  (
                                      [688] => 688
                                  )
          .....................................
                      [vendor] => Array
                          (
                              [5] => Array
                                  (
                                      [688] => 688
                                      [795] => 795
                                      [1817] => 1817
                                      [1818] => 1818
                                  )
          .....................................
          [resource] => Array
                  (
                      [parent-grandparents] => Array
                          (
                              [624] => Array
                                  (
                                      [688] => 688
                                      [1817] => 1817
                                      [1818] => 1818
                                      [795] => 795
                                  )
          .....................................
                      [parent-categories] => Array
                          (
                              [624] => Array
                                  (
                                      [688] => 688
                                      [1817] => 1817
                                      [1818] => 1818
                                      [795] => 795
                                  )
          .....................................
        • $this->methods=
          =Array
          (
              [ms|price] => number
              [ms|vendor] => vendors
              [resource|parent-grandparents] => grandparents
              [resource|parent-categories] => categories
          )
        • цикл по этим фильтрам
          • $ids =$this->filtersHandler->$method($requested, $values, $ids).    Т.е  "number,vendors, ...."
            • $requested - значения. диапазон цен, список id и т.д
            • $values - это кусок массива $this->filters, $this->filters[$table][$filter]. напр. $this->filters[''resource"]["parent-categories"]. содержит все ID исходные.
            • $ids - сначала исходный массив, потом остаток после каждого фильтра. Метод возвращает только если его результат есть в $ids.
            • $this->filter_operations++;
      • return $ids - это $matches.
    • $ids = array_intersect($ids, $matched);
    • $response = json_encode($response);
    • exit($response)

Мой опыт расширения фильтра


На практике, как часто бывает, все оказалось не так просто. Сразу проявилась явно ущербная структура товаров минишоп. Чтобы добавить поля, автор предлагает, добавить поле в таблицу, поменять модель, написать плагин и еще какие-то манипуляции. И после этого написать свои методы buid и filtr. И это не "черный юмор". Это на полном серьезе))).

У моего клиента около 10000 товаров. И общее число параметров под сотню. Естественно в отдельных категориях их не более 10-20. Клиент попросил сделать приязку параметров не к шаблонам, и тем более не к туповатой структуре минишопа, а к каталогам. Т.е. имеется таблица глобальных параметров. В каталоге выбираются чекбоксами нужные. И они появляются во всех дочерних товарах. 

Ну и чтобы работал фильтр Наумкина. 

Параметры я реализовал. Это отдельная тема. А вот с фильтром возникли сложности. Пришлось опять погрузиться в исходники.

Тем временем mFiltr2 продолжал удивлять дубовостью реализации - нельзя вот просто назначить свои фильтры. Надо написать класс-потомок класса mse2FiltersHandler. Через системные настройки поменять обаботчик фильтров на этот потомок. И в нем уже написать свои методы. Ну ладно, это не убьет меня). Хотя возникают вопросы). С другой стороны - меньше народу - больше кислороду). Времени было маловато, но потихоньку дело шло.

  • автоматически сгенерились у меня фильтры типа 
    cppvParamsInResurs|cppvFieldIdParam13:cppvFilterIdType4,
    cppvParamsInResurs|cppvFieldIdParam29:cppvFilterIdType1,  формат такой "таблица|поле:метод", метод иногда называется фильтром, иногда поле именуется фильтром. Но меня уже этим тут не удивить.
  • свой метод get будет $method = 'get' . ucfirst($table) . 'Values'; т.е getCppvParamsInResursValues, т.е. по имени таблицы, а не метода
  • build - $method = 'build' . ucfirst($filter) . 'Filter'; у нас это - buildCppvFilterIdType4Filter

Вроде все логично. Когда все запустишь и разберешься. А разобраться в дебрях безумкинского кода очень непросто. Постоянно меняется перетасовка переменных. Поле таблицы в фильтре часто именуется "методом" и т.д. Но если хочешь - разберешься. Тут оказалость главным чтобы метод(он же "фильтр") совпадал и в параметрах сниппета и в масссиве, который на выходе метода get. Я, для компактности, сократил имя в массиве. И огреб кучу проблем.

Повторим еще раз. Для именования get-метода используется имя таблицы(или псевдоним). Для build - имя метода фильтрации.

На входе get - массив полей и массив id ранее выбранных ресурсов. На выходе массив id ресурсов сгруппированных по имени метода фильтрации и по значениям полей.

На входе biuld - имя поля и массив значений этого поля. В моем случае для фильтра buildCppvFilterIdType4Filter

$values=Array
(
    [1] => Array
        (
            [688] => 688
            [1818] => 1818
        )

)

$name=cppvfieldidparam13
На выходе - массив для представления фильтра. Виджета, списка, чекбоксов и т.п.

И вот, наконец, после долгих мучений фильтр появился. Казалось бы поменять заголовок на вменяемый можно быстро через параметры. Ан нет. Надо, как оказалось, добавить запись в словари. Это лишний раз убедило меня в недоделанности и жесткости реализации. Ибо лезть постоянно в систему при настройке обычного сниппета как-то стремно.

Можно назначить в параметрах свои чанки. Например чанк контейнера для строк фильтров - &tplFilter.outer.таблица|поле.

А, поскольку предполагается что полей больше сотни и они динамические, назначение их как параметров сразу отпадает. 

Была шальная мысль - добавить в исходник вызов обработчика. Но сразу отогнал ее.  Единственный способ в этой ситуации, как мне кажется, это переопределить чанки по умолчанию. И там уж прехватить управление. Костыль на 100%. Но иного пути не вижу.

Если из чанка вызвать сниппет, то как параметры можно передать плейсхолдеры "table" и "filter". Причем "filter" - на самом деле "field"(поле таблицы). Код так уморочен, что только трассировщик и помогает.

И наконец сам фильтр - public function filterCppvFilterIdType4(array $requested, array $values, array $ids) . 

В формате filterИмяМетода. На входе массив запрошенных значений, массив значений по заданным полям, и массив ресурсов оставшийся от предыдущих фильтров. На выходе - итоговый массив подходящих под критерии ресурсов, с учетом предыдущих фильтров. Ну тут проблем не возникло и все отработало как надо. 

Как и обещал автор - можно подключить любые данные и любые методы фильтрации. Правда я б не сказал, то это просто. Ну это мое субъективное мнение. В итоге мне удалось реализовать привязку фильтра Наумкина к динамически назначаемым данным. Все реально генерится автоматически. Причем алгоритм вписался лучше чем для минишопа. Поскольку у глобальных параметров уже есть имена и не надо корячится со словарями. Единсвенно пришлось написать методы фильтрации для типов параметров. К счатью их немного. Это "число", "дата", "логика" и т.п.. Поскольку они почти все уже реализованы как стандртные, то можно просто перевызвать их.

Повторюсь - идея сама гениальнейшая. И это не преувеличение. Не сразу врубаешься в логику. Но это реально крутая идея. Потомучто непосредственно с реальными источниками данных имеет только метод getИмяТаблицыValues. Формируются стандартные массивы. Далее все однотипно для любых данных. И легко ими манипулировать.  Ну, а то что код не очень читабельный. Это уж личное дело программиста. У каждого свой стиль.  Несколько раз долго разбирался с "затыками", причиной коих был pdoTools. Забывал про его режим "fast", ограничивающий рекурсионный парсинг. Я не поклонник ни fenom, ни pdoTools. Мне по душе стандартные тэги и стандартный рекурсивный парсер. Но это, опять же, дело вкуса.

P.S.


В целом отличное расширение для MODX. Ничего подобно пока не встречал. Можно даже смело назвать гениальным. Еще бы хорошую документацию к нему. Хотя подавляющему большинству хватит и существующей. Только тщательное изучение исходников с трассировкой "расшифровало" заложенную идею. Уровень абстракции достаточен для расширения и подключения действительно любых данных и не только табличных. Можно дополнить обработку своими построителями, выборкой и фильтром. Алгортм фильтрации такой, очень кратко:

  1. На входе имеем массив id страниц. Полученные произвольным сниппетом или просто в параметре resources.
  2. Назначается список фильтров в формате таблица|поле:метод
  3. Исходный массив дублируется методом getИмяТаблицыКодовоеValues для каждого фильтра, но меняет структуру. Id группируютя по таблицам, именам полей и значеням полей каждого фильтра. Это позволяет легко делать выборки по критериям для вывода самих страниц и для подсчета "ожиданий". Все эти дубли сводятся в один массив ОбектФильтров->filters;
  4. При открытии страницы - генерится представление фильтров(обычно в левой колонке). Методом buildИмя_фильтраFilter. Далее все через AJAX.
  5. При смене критериев фильтра - меняется строка в адресе броузера и через AJAX запрашивается action.php. Страница не перегружается!!!
    1. запускается метод getИмяТаблицыКодовоеValues для каждого фильта
    2. запускается метод filterИмяФильтра для каждого фильтра. На вход передается уже "обрезанный" предыдущими фильтрами массив id.
    3. Вся выборка и прочее возвращаются в JSON
  6. JSON распихивается в контейнер для результатов. Если разрешено, то заполняются "ожидания".

Сожаление вызывает только привязка к pdoTools. Радует отсутствие привязки к fenom.


Комментарии 0






Разрешённые теги: <b><i><br>Добавить новый комментарий: