Расширенные поля пользователей

Небольшая пошаговая инструкция, как научиться сохранять любые интересующие данные о юзере в специальное поле extended.

На самом деле, это никакой не секрет. Во многих объектах MODX есть специальное поле типа JSON, которое хранится в БД как текст, например, properties у modResource и extended у modUserProfile.

При работе с объектами xPDO, JSON текст из этих полей превращается в массивы. То есть, общий принцип выглядит так:
// id нужного пользователя
$user_id = 15;
// Получаем объект modUser
if ($user = $modx->getObject('modUser', $user_id)) {
	// Получаем связанный с ним профиль пользователя
	if ($profile = $user->getOne('Profile')) {
		// Получаем специальное поле extended
		$extended = $profile->get('extended');
		// Добавляем новое значение
		$extended['mykey'] = 'mydata';
		// И сохраняем обратно в профиль
		$profile->set('extended', $extended);
		$profile->save();
	}
}
Самое приятное, что эти данные вы можете не только просмотреть на странице пользователя, но и изменить.


Регистрация и активность


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

Для плагина мы используем события OnUserSave и OnLoadWebDocument:
switch ($modx->event->name) {
	case 'OnUserSave':
		// Сохраняем дату создания нового пользователя
		if ($user && $mode == 'new') {
			if ($profile = $user->getOne('Profile')) {
				$extended = $profile->get('extended');
				$extended['registered'] = date('Y-m-d H:i:s');
				$profile->set('extended', $extended);
				$profile->save();
			}
		}
		break;
		
	case 'OnLoadWebDocument':
		// Сохраняем дату открытия любой страницы сайта, если пользователь авторизован
		if ($modx->user->isAuthenticated($modx->context->key)) {
			// Здесь мы работаем с текущим пользователем - у него профиль уже загружен
			$profile = $modx->user->Profile;
			$extended = $profile->get('extended');
			$extended['lastactivity'] = date('Y-m-d H:i:s');
			$profile->set('extended', $extended);
			$profile->save();
		}
		break;
}
И вот, что у нас получается:


Вывод полей профиля


Для вывода пользователей я советую использовать сниппет pdoUsers, который обладает всеми основными возможностями pdoTools.

Если вызвать его без чанка, то он распечатает массив всех плейсхолдеров юзера, включая поле extended.


pdoTools выводит массивы плейсхолдеров через точку, поэтому нас интересуют [[+extended.lastactivity]] и [[+extended.registered]].
[[!pdoUsers?
	&tpl=`@INLINE <p>[[+fullname]] - Регистрация: [[+extended.registered]], активность: [[+extended.lastactivity]]</p>`
]]


Можно использовать фильтры вывода или параметр &prepareSnippet, чтобы подготовить эти даты для вывода в более приятном виде.

Вот пример вывода с указанием &prepareSnippet:
[[!pdoPage?
	&element=`pdoUsers`
	&tpl=`tpl.Users.list.row`
	&prepareSnippet=`prepareUser`
]]

А вот и сам сниппет prepareUser:
<?php
// Комментарии юзера
$row['comments'] = $modx->getCount('TicketComment', array('createdby' => $row['id'], 'published' => 1));
// Тикеты
$row['tickets'] = $modx->getCount('Ticket', array('createdby' => $row['id'], 'class_key' => 'Ticket', 'published' => 1, 'deleted' => 0));
// Проверка и красивое форматирование дат через сниппет dateAgo
$row['registered'] = !empty($row['extended']['registered'])
	? $modx->runSnippet('dateAgo', array('input' => $row['extended']['registered']))
	: '-';
$row['lastactivity'] = !empty($row['extended']['lastactivity'])
	? $modx->runSnippet('dateAgo', array('input' => $row['extended']['lastactivity']))
	: '-';
	
return serialize($row);
&prepareSnippet — это особенность pdoTools, можно использовать со всеми сниппетами.

Недостатки


Из-за того, что в БД данные extended хранятся в виде JSON текста, по ним нельзя нормально осуществлять фильтрацию и сортировку.

Если вам нужно не просто выводить какие-то значения, то придётся написать дополнительную таблицу с id юзера и нужными колонками, чтобы сохранять данные в неё раздельно.

Тогда вы сможете присоединить эту таблицу в вызове сниппета pdoUsers и фильтровать\сортировать пользователяй по дополнительным полям.
Василий Наумкин
07 апреля 2014, 11:00
35
9 441
+11

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

Наумов Алексей
07 апреля 2014, 15:17
0
Спасибо! ну собственно так и начал делать…
Жаль, что не посортируешь.

Еще такой вопрос. Плагин висит на OnUserSave, а в HybridAuth нет его вызова: github.com/bezumkin/modx-hybridauth/search?q=OnUserSave&ref=cmdform

Зато есть OnUserFormSave — github.com/bezumkin/modx-hybridauth/search?q=OnUserFormSave&type=Code, который судя по описанию, вызывается при сохранению пользователя из админки. Нестыковочка какая-то, или я упустил что?
    Василий Наумкин
    07 апреля 2014, 15:20
    +2
    OnUserSave вызывается из самого modUser при сохранении.
      Андрей, Омск
      02 декабря 2015, 18:30
      0
      Василий, добрый день!
      Прошу совета: Хочу в Extended поле сохранить многострочный текст. Не понимаю, как сделать так, чтобы символы перевода строки учитывались. А то я получаю весь текст слепленный в одну строку.

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

      Буду очень признателен за помощь, сам ответ не смог найти.
      Пытался туда html засунуть, тоже режется при выводе в textarea
Наумов Алексей
07 апреля 2014, 17:13
0
А пример JOIN с кол-вом комментариев и публикаций можно попросить?
    Василий Наумкин
    07 апреля 2014, 17:15
    +1
    А сам?

    Ticket связан с TicketThread через Ticket.id — TicketThread.resource, а TicketThread с TicketComment через TicketThread.id — TicketComment.thread.
    Нужно 2 джойна и если комментариев много — будут тормоза.

    Такое присоединение есть в старых версиях getTickets, потом я от него отказался и выбираю количество комментариев отдельно.
      Наумов Алексей
      07 апреля 2014, 17:22
      0
      Да пишу сам, JOIN кол-ва комментариев происходит нормально, а когда еще публикации добавляю — ерунда.

      Вот пример:
      [[!getPage?
          &element=`pdoUsers`
          &groups=`Fisher`
          &limit=`15`
          &tpl=`users.List.Row`
          &sortby=`thislogin`
          &sortdir=`DESC`
          &where=`{"Comments.deleted = 0", "Comments.published = 1", "Publications.deleted = 0", "Publications.published = 1"}`
          &fastMode=`1`
          &select=`{"Comments":"COUNT(Comments.id) as comments_count", "Publications":"COUNT(Publications.id) as publications_count"}`
          &innerJoin = `{"Comments": {"class":"TicketComment","on":"Comments.createdby=modUser.id"}, "Publications": {"class":"modResource","on":"Publications.createdby=modUser.id"}}`
        ]]
      На выходе — откровенная фигня. Числа берутся не пойму откуда… но явно не те)

      p.s. если что это вопрос как тут modx.pro/users/ вывести кол-во постов и комментов.
        Василий Наумкин
        07 апреля 2014, 17:29
        0
        Проблема не в вызове сниппета, а в исходной задаче.

        Если нужно посчитать количество строк в двух разных таблицах — то нужна группировка и минимум 2 запроса.

        Лично я использую &prepareSnippet и считаю строки уже в нём — и быстро и удобно.
          Наумов Алексей
          07 апреля 2014, 17:35
          0
          Чет я не понял, это по 2 запроса на каждую строчку получится, при выводе 15 строк — лишних 30 запросов, не много?)
          А про постановку задачи понял… 1 джойн хорошо проходит, а 2 уже нельзя…
            Василий Наумкин
            07 апреля 2014, 17:38
            0
            Важно не количество запросов, а время их выполнения. Здесь показывает 30 запросов за 0.0677 сек. Как считаешь, много это или нет?

            А вот если написать запрос с двумя джоинами и подсчетом количества комментариев для каждой страницы — может быть и 5 секунд. Особенно, если 2000 страниц и 30000 комментариев.

            Короче, лучше много маленьких запросов, чем один большой.
              Наумов Алексей
              07 апреля 2014, 17:41
              0
              Ага! Сделал, реально 4 строчки и работает шустро. Спасибо.
                Василий Наумкин
                07 апреля 2014, 17:46
                0
                Молодец.

                Ну а я добавил эту информацию в заметку.
Володя
08 апреля 2014, 00:48
0
Спасибо! Познавательный мануал. Я с разбегу решил доп таблицу сделать под это дело и встрял на времени…
создал тип datetime но зараза туда только дата залетает…
время (часы, минуты, секунды) почему то все время по нулям присваивает…
ЧЯСНТ?
    Василий Наумкин
    08 апреля 2014, 05:54
    0
    Вставляешь-то как, через xPDO или SQL?

    Если xPDO — то покажи схему таблицы, если SQL — то запрос.
      Володя
      08 апреля 2014, 08:18
      0
      xPDO, я уже понял где лоханулся… схему изначально генерировал с типом DATE, а потом просто в базе тип менял. Ну естественно он мне только дату и проставлял…
      Володя
      08 апреля 2014, 09:51
      0
      схема
      <?xml version="1.0" encoding="UTF-8"?>
      <model package="Users" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" version="1.1">
      	<object class="UsersActivityMy" table="users_activity_my" extends="xPDOObject">
      		<field key="id" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" index="pk" />
      		<field key="registered" dbtype="datetime" phptype="datetime" null="true" />
      		<field key="lastactivity" dbtype="datetime" phptype="datetime" null="true" />
      		<index alias="PRIMARY" name="PRIMARY" primary="true" unique="true" type="BTREE" >
      			<column key="id" length="" collation="A" null="false" />
      		</index>
      	</object>
      </model>
      плагин
      <?php
      switch ($modx->event->name) {
      	case 'OnUserSave':
      		// Сохраняем дату создания нового пользователя
      		if ($user && $mode == 'new') {
      			if ($profile = $user->getOne('Profile')) {
      			    
      			    $id = $profile->get('id');
      			    //$modx->log(1, "id:  '{$id}'");
      			    
      			    $item = $modx->newObject('UsersActivityMy');
      			    $item->set('id',$id);
      			    $item->set('registered',date('Y-m-d H:i:s'));
      
      			    $item->save();
      			    $profile->save();
      
      			}
      		}
      		break;
      		
      	case 'OnLoadWebDocument':
      		// Сохраняем дату открытия любой страницы сайта, если пользователь авторизован
      		if ($modx->user->isAuthenticated($modx->context->key)) {
      			// Здесь мы работаем с текущим пользователем - у него профиль уже загружен
      			$id = $modx->user->id;
      
      			$item = $modx->getObject('UsersActivityMy',$id);
      
      			$item->set('lastactivity',date('Y-m-d H:i:s'));
      
                  $item->save();
      
      		}
      		break;
      }
      данные в базу записываются, все нормально.
      Теперь хочу присоединить таблицу к сниппету pdoUsers, делаю
      &loadModels=`Users`
      и в ответ получаю ругань
      [2014-04-08 09:46:27] (ERROR @ /index.php) Path specified for package users is not a valid or accessible directory: /var/www/sitename/www/core/components/users/model/
        Володя
        08 апреля 2014, 10:27
        0
        в extension_packages прописано
        {"Users":{"path":"[[++core_path]]components/Users/model/"}}
        Что еще не хватает?
          Володя
          08 апреля 2014, 11:14
          0
          думал косяк из за буквы заглавной, переименовал в usersactivity, один фиг та же ошибка.
          [2014-04-08 11:14:03] (ERROR @ /index.php) Path specified for package usersactivitymy is not a valid or accessible directory: /var/www/sitename/www/core/components/usersactivitymy/model/
        Василий Наумкин
        08 апреля 2014, 11:52
        0
        pdoTools предполагает, что ты пытаешься загрузить модель стандартного компонента.

        Если указываешь
        &loadModels=`users`
        Это значит, что файлы модели должны лежать в
        /core/components/users/model/users/
        Пример.

        Если же ты прописываешь загрузку модели в системную настройку extension_packages, то loadModels вообще указывать не нужно — должно работать сразу.
          Володя
          08 апреля 2014, 11:54
          0
          Там и лежат… Просто своим сниппетом данные выводятся. Но я никак теперь не допру как к выводу pdoUsers подключить эту свою таблицу.
            Володя
            08 апреля 2014, 12:05
            0
            p.s. То есть мне не нужно &loadModels=`users`?
            а достаточно
            [[!pdoPage?
            &element=`pdoUsers`
            &tpl=`@INLINE [[+username]] - [[+registered]] - [[+lastactivity]]`
            &leftJoin=`чего то там и тд`
            ]]
              Василий Наумкин
              08 апреля 2014, 12:09
              0
              Да. Если модель в памяти — ее не надо загружать отдельно.

              Например таблицы Tickets и MS2 присоединяются свободно и без loadModels.
                Володя
                08 апреля 2014, 12:19
                0
                почему то &showLog=`1` не пашет в pdoUsers
                  Василий Наумкин
                  08 апреля 2014, 12:26
                  +1
                  Ты поди авторизован в web и у юзера нет контекста mgr.

                  В любом случае, ошибки pdoTools пишутся в системный журнал.
                    Володя
                    08 апреля 2014, 12:28
                    0
                    аха… ты прав!
                    теперь осталось разобраться с лексиконом запроса и дело в шляпе!)
                    Я ж все обычно тупо копирую с твоих примеров вот эти лефтджойны и селекты..., а че это такое — не вникал.
                      Володя
                      08 апреля 2014, 12:55
                      0
                      вот так заработало, но я один фиг до конца не понял что к чему…
                      [[!pdoUsers?
                      &sortby=`modUser.id`
                      &tpl=`@INLINE [[+username]] - [[+registered]] - [[+lastactivity]]`
                      &showLog=`1`
                      
                      &leftJoin=`{"UsersActivityMy":{"class":"UsersActivityMy","alias":"UsersActivityMy","on":"UsersActivityMy.id = modUser.id"}}`
                      
                      &select=`{"UsersActivityMy":"`UsersActivityMy`.`registered`,`UsersActivityMy`.`lastactivity`"}`
                      ]]
                      тупо написал, а оно взяло и заработало…
                      Наумов Алексей
                      08 апреля 2014, 13:30
                      0
                      Владимир, как доделаете — напишите публикацию то по расширению modUser ;-)
                      Володя
                      08 апреля 2014, 13:34
                      0
                      Да что тут еще писать то? все что есть — выше тут же… Я никакой цели не преследовал, кроме как познавательной. Василий написал что сортировка только по собственной таблице — ну я и попробовал…
                      Что не ясно спрашивайте, я со своей стороны как малограмотный могу на пальцах обьяснить.)
                      Наумов Алексей
                      08 апреля 2014, 13:35
                      0
                      А вы просто таблицу создали, сам объект modUser не расширяли? Тогда да, ничего сложного…
                      Володя
                      08 апреля 2014, 13:37
                      0
                      так да… таблицу создал и все…
                      Василий Наумкин
                      08 апреля 2014, 20:16
                      +1
                      Молодец!

                      А у меня пришла в голову мысль, что пора сделать такую таблицу для юзеров в Tickets по умолчанию. Хотя бы активность и дату регистрации фиксировать.

                      Как будет время — попробую добавить.
    Андрей
    22 сентября 2014, 18:41
    0
    Делаю вывод пользователей. Выводить нужно только тех, у которых в Дополнительном поле профиля, некое значение.

    Как мне его вывести в данном куске кода?

    $q->where(array(
        'Profile.state' => "$search",
    // Здесь нужно добавить условие для дополнительного поля
    ));
    Василий Столейков
    04 сентября 2015, 08:24
    0
    Искал удобный готовый вариант для вывода какого-нибудь extended-поля в произвольном месте сайта, и в итоге сам написал его.
    Для удобного вывода extended-полей пользователя сделал себе сниппет extended в качестве фильтра.

    Пример вызова:
    [[!+modx.user.id:extended=`companyname`]]

    Код сниппета extended:
    <?php
    $user = $modx->getObject('modUser', $input);
    $profile = $user->getOne('Profile');
    $extended = $profile->get('extended');
    return $extended[$options];

    Данный вызов для текущего пользователя, но можно смело подставлять id любого пользователя.
    Может кому-нибудь пригодиться.
      Василий Столейков
      04 сентября 2015, 08:31
      0
      При таком подходе если значение extended не заполнено, то выводится id пользователя.
      Решил это следующим образом в сниппете:
      <?php
      $user = $modx->getObject('modUser', $input);
      $profile = $user->getOne('Profile');
      $extended = $profile->get('extended');
      if($extended[$options]) {
          return $extended[$options];
      }
      else {
          return ' ';
      }
      Не знаю, может есть и более красивое решение, чем подсовывать пробел на выводе…
        Василий Столейков
        04 сентября 2015, 12:48
        1
        0
        Ошибки и пустой экран, если пользователь неавторизован. Добавил проверку в сниппет:
        <?php
        $user = $modx->getObject('modUser', $input);
        if($user) {
            $profile = $user->getOne('Profile');
            $extended = $profile->get('extended');
        }
        if($extended[$options]) {
            return $extended[$options];
        }
        else {
            return ' ';
        }
          Konstantin
          09 января 2016, 04:26
          0
          Спасибо, выручил )
    Гриборий
    23 ноября 2016, 23:12
    0
    Приветствую. Было бы совсем здорово еще добавить новые поля в табличку на странице со всеми пользователями, с возможностью поиска и сортировки по ним.
      Гриборий
      24 ноября 2016, 02:02
      0
      Ошибся топиком, прошу прощения.
    Evgeny Epifanov
    20 июня 2017, 12:40
    0
    Подскажите, как передать extended поля в плагин при регистрации пользователя. Мне нужно при регистрации пользователя передать дополнительные значения из формы. Регистрация осуществляется через [[Login]]