Фильтрация пользователей с помощью mFilter2

Делая очередной тематический каталог организаций, где каждый пользователь это отдельная организация, которая размещает информацию о себе и своих услугах, я обычно применял классическое решение, когда при регистрации пользователя с помощью Office, создается отдельный ресурс и тогда можно без проблем просматривать карточки (ресурсы) организаций, осуществлять поиск и фильтровать их по разным параметрам используя готовые коробочные решения mSearch2 и mFilter2.

Плюсов тут несколько, в основном это возможность применять классические решения, ничего не допиливая. Но есть и минусы — приходится писать плагины, которые отслеживают изменение профиля пользователя и дублируют эти изменения в ресурс, определенные сложности модерирования, проблемы со скоростью сохранения и вывода информации и т.д.

Я решил заморочиться и сделал решение без дублирования информации, на основе таблицы пользователей.

В числе плюсов этого решения, могу привести следующие:
  • отсутствие дублирование информации и как следствие не нужно следить за связанными таблицами
  • возможность использовать группы и роли для сортировки пользователей и разграничения вывода информации (на основе групп можно реализовать разные тарифные планы например)

Прежде всего я понял, что стандартных полей таблицы пользователей мне не хватит, поэтому расширил таблицу, добавив свои дополнительные поля, с которыми MODX теперь работает как с родными. Об этом я писал здесь. Таким образом появился существенный прирост скорости, за счет того, что мне не нужно собирать и выводить многочисленные TV поля или присоединять еще какие то таблицы.

Далее появился вопрос, а как же этих пользователей фильтровать по различным общим признакам (специализация, рейтинг, PRO режим, страна, город и т.п.).
Само собой, чтобы не писать велосипеды было бы очень желательно использовать возможности mFilter2, но mFilter2 из коробки умеет фильтровать только ресурсы modResource и msProduct и связанные с ними таблицы (tv, msOption). Но спасибо Василию, за то, что он предусмотрел кастомизацию.

Задача, которую я перед собой поставил.
в таблице класса modUserProfile я имею добавленное мною дополнительное поле country_id, где лежит id страны из отдельной географической таблицы. Пользователи указывают в какой стране они работают, соответственно попробуем группировать и фильтровать пользователей по стране в качестве примера. Почему у меня хранится id страны, а не ее название? Ну просто сайт мультиязычный, и в зависимости от языка системы я выбираю из своей географической таблицы название на нужном языке. Из за этого придется делать дополнительный запрос к базе данных. Если у Вас нет необходимости в нескольких языках, то Вы можете хранить сразу название страны (такое поле кстати уже есть в стандартной таблице).

Шаг первый
Создаем страницу фильтрации и вызываем сниппет mFilter2 со следующими параметрами.
&tpl — просто выводим хоть что-то, чтобы видеть результат, если вы не укажете ничего, то сниппет распечатает результат запроса просто на экран в виде print_r
&element — вместо того, чтобы использовать стандартный сниппет mSearch2 который по умолчанию ищет в таблице site_content я указал pdoUsers, который как раз и выберет всех пользователей из таблицы пользователей (впервые пригодился). На данный момент наша конструкция уже выберет и выведет всех зарегистрированных пользователей в системе, автоматически разбив их постранично с помощью pdoPage.
&groups — Так как у меня кроме зарегистрированных компаний в таблице есть еще и другие пользователи, например администратор, мне нужно указать программе, что выбрать нужно только определенных пользователей. Для этого я добавляю нужных пользователей в отдельную группу shopkeepers и указываю сниппету, что выбрать нужно только их. Именно поэтому я и использую pdoUsers. Он уже из коробки умеет фильтровать пользователей по определенным параметрам. Далее мы будем просто передавать в него различные параметры &where и получать списки пользователей. Вот и все.
[[!mFilter2?
    &element=`pdoUsers`
    &groups=`shopkeepers`
    &tpl=`@INLINE Клиент - [[+fullname]]`   
]]
Шаг второй.
Так как мы договорились сортировать пользователей по странам (поле country_id), нужно построить соответствующий фильтр, выведя все указанные пользователями страны (предварительно нужно убедиться, что мы эти страны пользователям указали)

Стандартные коробочные фильтры mFilter2 нам не подходят, так как они могут построить фильтры только по ресурсам и связанными с ними полями. Придется переопределять стандартный класс и придумывать свои фильтры. Василий Наумкин уже предусмотрел такую возможность. Читаем Как работаю методы фильтрации и пример расширения фильтров

Согласно инструкции я создал свой класс, расширяющий стандартный класс фильтрации, указал его в системной настройке и осталось написать свои методы построения фильтра.

Добавляю в вызов сниппета новый фильтр &filters=`user|country_id:country`, где user это метод собирающий id пользователей, country_id — поле по которому строим фильтр, country — новый метод фильтрации
[[!mFilter2?
    &element=`pdoUsers`
    &groups=`shopkeepers`
    &tpl=`@INLINE [[+fullname]]`
    &filters=`
       user|country_id:country
    `
]]
Ниже привожу методы моего файла core/components/msearch2/custom/filters/custom.class.php
Первый метод выбирает всех пользователей, у которых есть заполненное поле country_id, затем собирает уникальные значения (id стран). На выходе получаем список стран, которые встречаются в базе.
public function getUserValues(array $fields, array $ids) {
		$filters = array();
		$no_id = false;
		if (!in_array('id', $fields)) {
			$fields[] = 'id';
			$no_id = true;
		}
		$q = $this->modx->newQuery('modUser');
		$q->leftJoin('modUserProfile','profile', 'profile.internalKey = modUser.id');
		$q->orCondition(array('profile.internalKey:IN' => $ids));
	    foreach($fields as $field){
	        $q->select('profile.'.$field);
	    }

		if ($q->prepare() && $q->stmt->execute()) {
			while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
			    // $row =  один пользователь и выборка его полей
				foreach ($row as $k => $v) {
				    // $k Название поля например country_id
				    // $v = его значение например 31
					$v = str_replace('"', '"', trim($v));
					if ($v == '' || $v == '0' || $k == 'id' ) {
						continue;
					}
					elseif (isset($filters[$k][$v])) {
						$filters[$k][$v][$row['id']] = $row['id'];
					}
					else {
						$filters[$k][$v] = array($row['id'] => $row['id']);
					}
				}
			}
		}
		else {
			$this->modx->log(modX::LOG_LEVEL_ERROR, "[mSearch2] Error on get filter params.\nQuery: ".$q->toSql()."\nResponse: ".print_r($q->stmt->errorInfo(),1));
		}

		return $filters;
	}
Второй метод отвечает за построение фильтра. На входе получаем массив с ids пользователей и ids стран. На выходе получаем готовый фильтр включающий в себя названия стран и счетчик показывающий сколько пользователей указали ту или иную страну.
public function buildCountryFilter(array $values, $name = '', $field = 'country_id') {
		// $values - массив где key - страна, value - список пользователей у которых указана эта страна

		$q = $this->modx->newQuery('modUserProfile');
		$q->select('id,'.$field);
		if ($q->prepare() && $q->stmt->execute()) {
			// Здесь имею массив пользователей с заполненной страной, где key user id а value country id
			foreach ($values as $country_id => $ids) {
				// Здесь делаю дополнительный запрос для получения названия страны по ее id
				$sql = "SELECT ru_title FROM `modx_geo_content` WHERE id = :id";
                		$q = $this->modx->prepare($sql);
               			 $q->bindParam(':id', $country_id);
                		$q->execute();
               			 $arr = $q->fetchAll(PDO::FETCH_ASSOC);
               			 $title = $arr[0]['ru_title'];

				$results[$title] = array(
					'title' => $title,
					'value' => $country_id,
					'type' => $field,
					'resources' => $ids
				);
			}
		}
		ksort($results);

		return $results;
	}

В итоге мы получили первоначальный список пользователей которые входят в указанную группу (вы можете указать дополнительные условия выборки через &where) и построенный фильтр содержащий список всех встречающихся у пользователей стран.

Шаг третий
Если вы попробуете выбрать одну из стран, отметив ее в фильтре, то заметите, что сортировка не происходит. Нужный параметр в адресной строке добавляется, но список пользователей остается тем же.

Давайте разберемся почему так происходит.
После отметки страны, происходит ajax обращение к сниппету pdoUsers, но параметр из адресной строки никак не учитывается (pdoUsers не умеет читать эти параметры), поэтому мы получаем первоначальный список пользователей и не видим изменений. Нам нужно считать параметр из адресной строки ?user|country_id=93, содержащий id страны, по которой нужно сделать сортировку и передать ее в запрос через параметр $where

Для этого нужно сделать так называемый сниппет обертку, в котором мы считываем параметры адресной строки, записываем их в параметр $where и вызываем pdoUsers с обновленными параметрами
<?php
if(isset($_GET['user|country_id'])){
    $country_ids = $_GET['user|country_id'];
    if (!empty($where)) {
        if (!is_array($where)) {
            $where = $modx->fromJSON($where);
        }
    } else {
        $where = array();
    } 
    
    $where['modUserProfile.country_id:IN'] = !is_array($country_ids)
        ? array_map('trim', explode(',', $country_ids))
        : $country_ids;
    if (!empty($where)) {
        
        $scriptProperties['where'] = $modx->toJSON($where);
    }
    
}
return $modx->runSnippet('pdoUsers', $scriptProperties);
Ну и теперь в параметре &element нужно вместо pdoUsers нужно вызвать наш сниппет обертку. В таком случае мы все равно вызываем pdoUsers, но еще и учитываем GET параметры.

[[!mFilter2?
    &element=`getUsersFilter`
    &groups=`shopkeepers`
    &tpl=`@INLINE [[+fullname]]`
    &filters=`
       user|country_id:country
    `
]]
Теперь сортировка по стране у меня заработала. Остается только подготовить чанк для вывода информации. Кстати, для того, чтобы просмотреть подробную информацию о отдельной карточке пользователя я буду использовать вот эту технику

Жду ваших комментариев и оценок проделанной работы, наверняка будут советы по оптимизации логики.
Николай Савин
03 апреля 2016, 08:04
modx.pro
17
4 494
+4
Поблагодарить автора Отправить деньги

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

Василий Наумкин
04 апреля 2016, 05:22
+3
По идее, не нужно что-то получать из $_GET и фильтровать самостоятельно.

Сниппет-обёртка должен получать id подходящих юзеров в параметре &resources, через запятую.
    Василий Наумкин
    04 апреля 2016, 11:19
    +2
    Можно, кстати, прямо скопипастить из вот этой заметки
    <?php
    /**@var array $scriptProperties */
    if (!empty($resources)) {
        if (!empty($where)) {
            if (!is_array($where)) {
                $where = $modx->fromJSON($where);
            }
        } else {
            $where = array();
        }
        $where['id:IN'] = !is_array($resources)
            ? array_map('trim', explode(',', $resources))
            : $resources;
        if (!empty($where)) {
            $scriptProperties['where'] = $modx->toJSON($where);
        }
    }
    
    return $modx->runSnippet('pdoUsers', $scriptProperties);
      Николай Савин
      04 апреля 2016, 11:41
      0
      Спасибо работает. На днях буду допиливать другие параметры сортировки, протестирую сниппет на универсальность.
    Asert
    10 ноября 2016, 17:22
    0
    Интересный материал.
    Только вот проблема в том что данный метод только для полей которые заданы для таблицы UserProfile
    Для встроенных полей пользователя возможно сделать фильтрацию?
    К примеру поле country, city которые в таблице user_atributes
      Николай Савин
      10 ноября 2016, 17:46
      +1
      Нет такой таблицы UserProfile, все данные о которых идет речь, лежат как раз в таблице user_atributes.
      А UserProfile это объект, который получается из таблицы и содержит данные таблицы user_atributes.
      Так что фильтация по дефолтным полям пользователя (city, country) через этот способ тоже поддерживается
        Asert
        10 ноября 2016, 18:02
        0
        Не могли бы помочь не могу понять что необходимо заменить в файле который вы создали
        Сейчас файл custom.class.php выглядит следующим образом
        <?php
        class myCustomFilter extends mse2FiltersHandler {
        public function getUserValues(array $fields, array $ids) {
        		$filters = array();
        		$no_id = false;
        		if (!in_array('id', $fields)) {
        			$fields[] = 'id';
        			$no_id = true;
        		}
        		$q = $this->modx->newQuery('modUser');
        		$q->leftJoin('modUserProfile','profile', 'profile.internalKey = modUser.id');
        		$q->orCondition(array('profile.internalKey:IN' => $ids));
        	    foreach($fields as $field){
        	        $q->select('profile.'.$field);
        	    }
        
        		if ($q->prepare() && $q->stmt->execute()) {
        			while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
        			    // $row =  один пользователь и выборка его полей
        				foreach ($row as $k => $v) {
        				    // $k Название поля например country_id
        				    // $v = его значение например 31
        					$v = str_replace('"', '"', trim($v));
        					if ($v == '' || $v == '0' || $k == 'id' ) {
        						continue;
        					}
        					elseif (isset($filters[$k][$v])) {
        						$filters[$k][$v][$row['id']] = $row['id'];
        					}
        					else {
        						$filters[$k][$v] = array($row['id'] => $row['id']);
        					}
        				}
        			}
        		}
        		else {
        			$this->modx->log(modX::LOG_LEVEL_ERROR, "[mSearch2] Error on get filter params.\nQuery: ".$q->toSql()."\nResponse: ".print_r($q->stmt->errorInfo(),1));
        		}
        
        		return $filters;
        	}
        	public function buildCountryFilter(array $values, $name = '', $field = 'country_id') {
        		// $values - массив где key - страна, value - список пользователей у которых указана эта страна
        
        		$q = $this->modx->newQuery('modUserProfile');
        		$q->select('id,'.$field);
        		if ($q->prepare() && $q->stmt->execute()) {
        			// Здесь имею массив пользователей с заполненной страной, где key user id а value country id
        			foreach ($values as $country_id => $ids) {
        				// Здесь делаю дополнительный запрос для получения названия страны по ее id
        				$sql = "SELECT ru_title FROM `modx_geo_content` WHERE id = :id";
                        		$q = $this->modx->prepare($sql);
                       			 $q->bindParam(':id', $country_id);
                        		$q->execute();
                       			 $arr = $q->fetchAll(PDO::FETCH_ASSOC);
                       			 $title = $arr[0]['ru_title'];
        
        				$results[$title] = array(
        					'title' => $title,
        					'value' => $country_id,
        					'type' => $field,
        					'resources' => $ids
        				);
        			}
        		}
        		ksort($results);
        
        		return $results;
        	}
        }
        Что здесь необходимо заменить что бы фильтр работал по полю country и city
          Asert
          10 ноября 2016, 18:19
          0
          Заменив country_id на country ничего не выводит.
      Asert
      10 ноября 2016, 21:40
      0
      Сделал вот так но в фильтре отображаются id пользователей

      <?php
      class myCustomFilter extends mse2FiltersHandler {
      public function getUserValues(array $fields, array $ids) {
      		$filters = array();
      		$no_id = false;
      		if (!in_array('id', $fields)) {
      			$fields[] = 'id';
      			$no_id = true;
      		}
      		$q = $this->modx->newQuery('modUser');
      		$q->leftJoin('modUserProfile','profile', 'profile.internalKey = modUser.id');
      		$q->orCondition(array('profile.internalKey:IN' => $ids));
      	    foreach($fields as $field){
      	        $q->select('profile.'.$field);
      	    }
      
      		if ($q->prepare() && $q->stmt->execute()) {
      			while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
      			    // $row =  один пользователь и выборка его полей
      				foreach ($row as $k => $v) {
      				    // $k Название поля например country_id
      				    // $v = его значение например 31
      					$v = str_replace('"', '"', trim($v));
      					if ($v == '' || $v == '0' || $k == 'country' ) {
      						continue;
      					}
      					elseif (isset($filters[$k][$v])) {
      						$filters[$k][$v][$row['country']] = $row['id'];
      					}
      					else {
      						$filters[$k][$v] = array($row['country'] => $row['id']);
      					}
      				}
      			}
      		}
      		else {
      			$this->modx->log(modX::LOG_LEVEL_ERROR, "[mSearch2] Error on get filter params.\nQuery: ".$q->toSql()."\nResponse: ".print_r($q->stmt->errorInfo(),1));
      		}
      
      		return $filters;
      	}
      	public function buildCountryFilter(array $values, $name = '', $field = 'country') {
      		// $values - массив где key - страна, value - список пользователей у которых указана эта страна
      
      		$q = $this->modx->newQuery('modUserProfile');
      		$q->select('country,'.$field);
      		if ($q->prepare() && $q->stmt->execute()) {
      			// Здесь имею массив пользователей с заполненной страной, где key user id а value country id
      			foreach ($values as $country => $country) {
      				// Здесь делаю дополнительный запрос для получения названия страны по ее id
      							$sql = "SELECT country FROM `modx_user_atributes` WHERE id = :country";
                      		$q = $this->modx->prepare($sql);
                     			 $q->bindParam(':id', $country);
                      		$q->execute();
                     			 $arr = $q->fetchAll(PDO::FETCH_ASSOC);
                     			 $title = $arr[0]['country'];
      
      				$results[$title] = array(
      					'title' => $title,
      					'value' => $country,
      					'type' => $field,
      					'resources' => $ids
      				);
      			}
      		}
      		ksort($results);
      
      		return $results;
      	}
      }
      Может кто то помочь?
        Николай Савин
        10 ноября 2016, 21:43
        0
        Попробуй так, метод фильтрации не прописывается или прописывается :default
        [[!mFilter2?
            &element=`getUsersFilter`
            &groups=`shopkeepers`
            &tpl=`@INLINE [[+fullname]]`
            &filters=`
               user|city
            `
        ]]
          Asert
          10 ноября 2016, 21:54
          0
          Появилось в фильтре mse2_filter_user_city и город отображается
          И второй фильтр mse2_filter_user_id вместо страны в нем отображаются id пользователей
            Asert
            10 ноября 2016, 21:56
            0
            Пользователи отображаются но при нажатии не фильтрует их.
              Николай Савин
              10 ноября 2016, 21:59
              0
              Шаг третий из статьи делал? Только там учти что у тебя Get параметр другой будет
                Asert
                10 ноября 2016, 22:12
                0
                Шаг третий делал
                Но переделать не получается под свои требования. Изменил все country_id uf country но не заработало.
                  Asert
                  10 ноября 2016, 22:14
                  0
                  С этим сниппетом заработала фильтрация
                  <?php
                  /**@var array $scriptProperties */
                  if (!empty($resources)) {
                      if (!empty($where)) {
                          if (!is_array($where)) {
                              $where = $modx->fromJSON($where);
                          }
                      } else {
                          $where = array();
                      }
                      $where['id:IN'] = !is_array($resources)
                          ? array_map('trim', explode(',', $resources))
                          : $resources;
                      if (!empty($where)) {
                          $scriptProperties['where'] = $modx->toJSON($where);
                      }
                  }
                  
                  return $modx->runSnippet('pdoUsers', $scriptProperties);
                    Николай Савин
                    10 ноября 2016, 22:15
                    0
                    Вот только хотел его скинуть. Молодец что сам нашел.
          Asert
          10 ноября 2016, 22:14
          0
          Помогите фильтр по стране сделать
          Asert
          10 ноября 2016, 22:26
          0
          Фильтрация зарабтала только вот страны не выводятся вместо них id пользователей
            Николай Савин
            10 ноября 2016, 22:37
            0
            А сортировка по городам я так понимаю работает?
            Для сортировки по странам, придется писать свой отдельный метод фильтрации, мой не подойдет, я использовал собственную базу стран, не стандартную базу стран MODX.
            Вызов метода будет примерно таким
            [[!mFilter2?
                &element=`getUsersFilter`
                &groups=`shopkeepers`
                &tpl=`@INLINE [[+fullname]]`
                &filters=`
                   user|country:country
                `
            ]]
            В классе фильтрации нужно будет написать свой отдельный метод buildCountryFilter, который будет выводить названия стран. Мой метод из примера не подойдет, потому что я брал данные из своей отдельной таблицы, но можно его просто допилить под использование стандартной таблицы стран
              Asert
              10 ноября 2016, 22:49
              0
              Можете помочь в этом? Допилить ваш метод.
                Николай Савин
                10 ноября 2016, 22:54
                +1
                К сожалению перегружен работой. Даже за деньги некогда. Попробуй разместить заявку в разделе работа с заголовком Доработать метод фильтрации и ссылкой сюда.
                  Asert
                  10 ноября 2016, 23:18
                  0
                  Вернул все значения какие были в вашем файле и все заработало.
                  Спасибо за помощь.
            Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
            22