Создание счетчика посещений для сайта на MODX

Создание счетчика посещений для сайта на MODX
Содержание:
  1. Краткий план
  2. Шаг 1. Создание TV для хранения просмотров
  3. Шаг 2. Создание плагина для обновления TV
  4. Шаг 3. Создание сниппета для вывода самых популярных ресурсов
  5. Комментарии

В этой статье изучим как можно очень просто добавить на сайт, работающим под управлением CMS MODX, подсчет количества просмотров страниц.

Краткий план

Подсчет количества просмотров страниц на сайте организуем так:

  • само значение будем хранить в TV поле views;
  • его обновление будем выполнять посредством плагина UpdateTVViews;
  • для получения списка самых просматриваемых ресурсов напишем сниппет getMostViewedResources;
  • для вывода результатов работы сниппета getMostViewedResources будем использовать чанки: tplMostViewedResourcesRow (для одного элемента) и tplMostViewedResources (в качестве обёртки, для заворачивания всех элементов).

Защиту от накрутки выполним с использованием куки.

В коде шаблона получить количество просмотров ресурса можно будет посредством плейсхолдера [[!+views]], который будем устанавливать с помощью плагина UpdateTVViews.

Вывести список самых популярных ресурсов можно будет посредством вызова сниппета [[!getMostViewedResources]].

Шаг 1. Создание TV для хранения просмотров

Разработку в MODX начнем с создания дополнительного поля и его настройки следующим образом:

  • на вкладке «Общая информация» в «Название» введём views и в «Подпись» – Количество просмотров;
  • на вкладке «Параметры ввода» в выпадающем списке «Тип ввода» выберем Число, а в поле «Номер по умолчанию» введём значение 0;
  • на вкладке «Параметры вывода» в выпадающем списке «Тип ввода» выберем значение По умолчанию;
  • на вкладке «Доступно для шаблонов» отметим галочками шаблоны, для которых это TV должно быть доступно.
Создание дополнительного поля views в MODX

После этого в ресурсах, которые используют один из шаблонов для которых это TV доступно, на вкладке «Дополнительные поля (TV)» появится только что созданное TV-поле views.

Отображение TV поля views в форме создания ресурса

Шаг 2. Создание плагина для обновления TV

После добавления TV в MODX создадим новый плагин и дадим ему название UpdateTVViews. Он будет использоваться для увеличения счетчика просмотров views.

Плагин будет выполняться при наступлении системного события OnLoadWebDocument, которое будет возникать после загрузки документа, но до обработки парсером тегов MODX. Получение текущего ресурса при наступлении этого события можно получить посредством $modx->resource.

Код плагина UpdateTVViews:

<?php
if ($modx->event->name != 'OnLoadWebDocument') {
  return;
}

// получим ресурс
$resource = $modx->resource;
// URL
$url = $modx->makeUrl($resource->get('id'), '', '', 'abs');
// id TV views
$tvViewsId = 1;
// количество просмотров
$views = 0;

// если TV не доступна для этого шаблона, то завершаем
$tvViewsTemplate = $modx->getObject('modTemplateVarTemplate', [
  'tmplvarid' => $tvViewsId,
  'templateid' => $resource->get('template')
]);
if (!$tvViewsTemplate) {
  return;
}

// получаем TV
$tvViews = $modx->getObject('modTemplateVarResource', [
  'tmplvarid' => $tvViewsId,
  'contentid' => $resource->get('id')
]);

// если имеется TV то получаем его значение, иначе создаем
if ($tvViews) {
  $views = $tvViews->get('value');
} else {
  $tvViews = $modx->newObject('modTemplateVarResource');
  // устанавливаем значение поля tmplvarid
  $tvViews->set('tmplvarid', $tvViewsId);
  // устанавливаем значение поля contentid
  $tvViews->set('contentid', $resource->get('id'));
}

// если значение куки не равно текущему URL, то увеличиваем счетчик
if ($_COOKIE['views'] != $url) {
  // устанавливаем количество просмотров
  $tvViews->set('value', ++$views);
  // сохраняем TV
  $tvViews->save();
  // устанавливаем COOKIE views
  setcookie('views', $url, time() + (86400 * 365), $url);
}

// устанавливаем плейсхолдер views
$modx->setPlaceholder('views', $views);

Для того чтобы плагин отслеживал данное событие, его необходимо включить на вкладке «Системные события»:

Установка событий, которые должен отслеживать плагин UpdateTVViews

В коде, приведенном выше, для того чтобы счётчик просмотров не увеличивался при повторном просмотре некоторого ресурса одним и тем же пользователем используются куки. Установка куки выполняется посредством php-функции setcookie, а её получение с помощью ассоциативного массива $_COOKIE.

Кроме этого, плагин будет ещё устанавливать плейсхолдер views, посредством которого можно будет вывести количество просмотров ресурса на экран. Плейсхолдер необходимо вызывать не кэшированным:

[[!+views]]

Шаг 3. Создание сниппета для вывода самых популярных ресурсов

В завершении напишем сниппет для вывода самых популярных страниц на сайте. Назовем его getMostViewedResources. Сниппет будет получать из базы 5 ресурсов, которые имеют самые большие значения views.

Код сниппета getMostViewedResources:

// id TV views
$tvViewsId = 1;

// создадим новый запрос, используя объект modTemplateVarResource
$query = $modx->newQuery('modTemplateVarResource');
// присоединяем к нему объект modResource
$query->innerJoin('modResource', 'Resource');
// добавляем к запросу условие
$query->where(['modTemplateVarResource.tmplvarid' => $tvViewsId]);
// выполним сортировку по TV (для преобразования строки в беззнаковое число используем mysql функцию CAST)
$query->sortby('CAST(modTemplateVarResource.value as unsigned)', 'DESC');
// укажем поля для выборки
$query->select([
  'Resource.id as id',
  'Resource.pagetitle as pagetitle',
  'modTemplateVarResource.value as countviews'
]);
// ограничим выборку 5 записями
$query->limit(5);
// подготовим и выполним запрос
$query->prepare();
$query->stmt->execute();
$rows = $query->stmt->fetchAll(PDO::FETCH_ASSOC);
// присвоим переменной $output пустую строку
$output = '';
// переберём все записи
foreach ($rows as $row) {
  $row['url'] = $modx->makeUrl($row['id'], '', '', 'abs');
  // оформим вывод строки с использованием чанка tplMostViewedResourcesRow
  $output .= $modx->getChunk('tplMostViewedResourcesRow', $row);
}
$output = $modx->getChunk('tplMostViewedResources', ['output' => $output]);
// вернём результат
return $output;

HTML-шаблон который используется для формирования одной строчки хранится в чанке tplMostViewedResourcesRow:

<li class="list-group-item d-flex justify-content-between align-items-start">
  <div class="ms-2 me-auto">
    <div class="fw-bold"><a href="[[+url]]">[[+pagetitle]]</a></div>
  </div>
  <span class="badge bg-primary rounded-pill">[[+countviews]]</span>
</li>

Чанк tplMostViewedResources используется в качестве обёртки и имеет следующий код:

<div class="card">
  <div class="card-header">Популярные статьи</div>
  <ol class="list-group list-group-flush list-group-numbered">[[+output]]</ol>
</div>

Для вывода блока с самыми популярными страницами необходимо поместить в необходимое место шаблона вызов сниппета getMostViewedResources:

[[!getMostViewedResources]]

Вид блока «Популярные статьи»:

Блок для сайта с самыми просматриваемыми страницами на ресурсе

Оформление этого блока выполнена с использованием Bootstrap 5, а для создания структуры этого списка был выбран компонент из этого фронтенд инструмента под названием List group.

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

vmxmv
vmxmv

Здравствуйте! Подскажите, как исключить страницу из данного вывода? Сейчас у меня всегда на первом месте главная страница.

Reut
Reut

Здравствуйте!

Подскажите, как вывести в чанке tplMostViewedResourcesRow другие TV поля, помимо pagetitle?? Нужно вывести картинку и т.д по желанию... может в сниппет добавить нужно как-то?
Александр Мальцев
Александр Мальцев

Привет! Если это TV-поле, то его необходимо добавить к запросу с помощью, например, leftJoin.

vmxmv
vmxmv

это делается в сниппете getMostViewedResources? Нужно просто добавить аналогично как здесь? $query->innerJoin('modResource', 'Resource'); только с помощью leftJoin?

Nik
Nik

Добрый день Александр! Подскажите как можно сделать в вашем решении, чтобы статистику можно было переключить на кол-во просмотров только за день (сутки)?

То есть выбрал период: день, и показывает только текущие просмотры ресурсов (за день)
Александр Мальцев
Александр Мальцев

Привет! Тут никак, ну если только за текущие сутки. В этом случае нужно просто очищать это TV-поле у всех ресурсов, когда начинается новый день.

Nik
Nik

Да, так и сделал, очистка вручную, если интересно допустим посмотреть что просматривают за день. Небольшой код:

$value = 0;
$tvs = $modx->getIterator('modTemplateVarResource', array('tmplvarid' => 1));
foreach($tvs as $tv){
    $path = $tv->get('value');
    $path = str_replace($path,$value,$path);
    $tv->set('value',$path);
    $tv->save();
}

Где 'tmplvarid' =>, id tv-шки в которой надо обнулить значения

Использовал вот эту статью - ИТ Шеф

Хотел через сниппет, чтобы по кнопке очищалось, но проще через консоль, выполнил когда надо. Может пригодиться кому.

Кстати отличный счетчик, ничего лишнего, все шустро работает, и на примере по изучать одно удовольствие. Спасибо!
Александр Мальцев
Александр Мальцев

Пожалуйста! Рад что всё получилось и поделились своим решением.

АлексейВитальевич
АлексейВитальевич
Доброго времени суток, Александр!
Спасибо за статью да и вообще за работу сайта itchief — очень много полезной информации.
Вопрос по сабжу — подключил количество просмотров. Но вместо варианта со сниппетом для вывода «популярных статей» использую вывод через pdoResources с сортировкой по TV «views». Сортировка работает. Есть ли большая разница в сортировке через pdoResources и сниппетом указаном в статье?

PS в Modx недавно
Александр Мальцев
Александр Мальцев
Добрый день! Спасибо за отзыв!
Использовать своё решение или сторонний компонент, это вопрос не только MODX, но и любых систем. Своё решение нужно написать, как, например, в этой статье. Это намного дольше и менее универсальнее, чем использовать какой-то инструмент, например, такой как pdoResources. Но за потраченное время вы также получаете преимущества:
1. В производительности. Сниппет в статье предназначен четко для выполнения одной этой задачи. Он будет выполняться максимально быстро. Инструмент, который в данном случае является pdoResources, является универсальным. Он предназначен для выполнения большого количества различных задач. Так как размер его кода намного больше, чем сниппета в этой статье, то и выполняться он будет гораздо дольше.
2. В кастомизации. В сниппете я могу написать что угодно при необходимости, готовый компонент работает в рамках его функционала.
3. В независимости от сторонних решений. Например, если автор pdoResources не выпустит обновления для новой версии MODX, то вы не сможете обновиться. Или, например вы хотите перейти на новую версию PHP, и сама MODX с ней совместима, а этот компонент нет.
Y2507
Y2507
Добрый день!
Такой вопрос в тикете выводится количество просмотров, а как вывести в раздел с тикетами там не работает, переделал как вы сделали новый плагин viewsResource,

Заметил что у вас на сайте тоже странный подсчет, в разделе с тикетами 1.7K а если зайти в статью то там 101
Y2507
Y2507
Добрый день!
Такой вопрос в тикете выводится количество просмотров, а как вывести в раздел с тикетами там не работает, переделал как вы сделали новый плагин viewsResource,

Заметил что у вас на сайте тоже странный подсчет, в разделе с тикетами 1.7K а если зайти в статью то там 101
Олег
Олег
Александр, установил HitsPage, все ОК.
Но возникла проблема.
MODX «закалена», папка CORE вынесена за корневую директорию.
А после установки HitsPage в корне система создает вторую папку CORE с файлами HitsPage.
Как установить компонент вне корня?
Александр Мальцев
Александр Мальцев
Скорее всего при установке компонента путь к папке core прописан «жёстко». Нужно чтобы разработчик компонента поправил путь к папке core так, чтобы он брался из константы MODX_CORE_PATH.
Дмитрий
Дмитрий
Александр, здравствуйте!
А есть код который просто бы учитывал просмотр определенной ссылки на странице сайта с несколькими такими же, но разными ссылками и количество выводилось бы отдельно каждой ссылки через html код?
Подскажите пожалуйста.
Александр Мальцев
Александр Мальцев
Здравствуйте!
Наиболее простой вариант – это добавить к ссылкам utm метки. Если нужно просто статистика, то можно это выполнять через Google Analytics. Если нужно выводить на странице, то тоже можно попробовать получать эти данные из этого сервиса.
Ну или пойти по обычному пути, а именно написать свой компонент для MODX, решающий эту задачу. Если для хранения данных подойдет TV, то можете выполнить это через них.
Дмитрий
Дмитрий
Благодарю вас! Видимо да, нужно изучить MODX, но пока что я не имею понятия, что это)
Александр Мальцев
Александр Мальцев
Да, если требуется функционал которого нет по умолчанию в MODX и готового компонента для этого нет, то без изучения MODX тут уже не обойтись.
kalisto
kalisto
И у меня не работает счетчик страниц.
Александр
Александр
Добрый день, подсчет заработал, подскажите, пожалуйста, а как вывести популярные статьи из определенной категории?
Александр Мальцев
Александр Мальцев
Добрый день, Александр.
В этом случае необходимо немного доработать сниппет topTicketsView:
<?php
$parent = $modx->getOption('parent', $scriptProperties, 0);
$output = $modx->cacheManager->get('topticketview-'.$parent);
if (empty($output)) {
  $query = $modx->newQuery('modTemplateVarResource');
  $query->innerJoin('modResource','Resource');
  $query->where(array(
    'modTemplateVarResource.tmplvarid'=>'2',
    'Resource.class_key' => 'Ticket'
  ));
  if ($parent != 0) {
    $query->where(array(
      'Resource.parent' => $parent
    ));  
  }
  $query->sortby('CAST(modTemplateVarResource.value as unsigned)',DESC);
  $query->select(array(
    'Resource.pagetitle as pagetitle',
    'Resource.uri as uri',
    'modTemplateVarResource.value as countviews'
  ));
  $query->limit(5);
  $query->prepare();
  $query->stmt->execute();
  $rows = $query->stmt->fetchAll(PDO::FETCH_ASSOC);
  $output = '';
  $idx = 0;
  foreach ($rows as $row) {
    $idx++;
    $row['idx'] = $idx;
    $output .= $modx->getChunk('tpl.top.views.tickets',$row);
  }
  $modx->cacheManager->set('topticketview-'.$parent,$output,10800);
}
return $output;
Теперь при вызове сниппета нам доступен параметр parent. Если его не устанавливать, то выборка не будет ограничиваться. А если ему указать id категории, то выбор тикетов будет ограничиваться ей.
<!-- Самые популярные ресурсы на сайте -->
[[!topTicketsView]]
<!-- Самые популярные ресурсы в секции, имеющей id = 15
[[!topTicketsView? &parent=`15`]]
Александр
Александр
Всё супер! Заработало! Спасибо вам огромное!
Демьян Золин
Демьян Золин
1. Создал TV
2. Создал плагин
3. в Чанк «Footer» добавил
[[!+viewsTicket]]
[[!+viewsSection]]
[[*views]]
В результате видно не меняющуюся цифру «0» от этого [[*views]]
Александр Мальцев
Александр Мальцев
Скорее всего, у вас ресурс не является Ticket.
Если вам необходимо организовать плагин для обычных ресурсов (modDocument), то вам нужно вместо Ticket использовать modDocument:
// если тип ресурса == modDocument
if ($class_key == 'modDocument')
Лучше не использовать [[*views]], т.к. он создаст дополнительные запросы к базе данных и увеличит время парсинга страницы. Используйте подготовленный плейсхолдер [[!+viewsTicket]].
Демьян Золин
Демьян Золин
Верно ресурс не является Ticket.
// если тип ресурса == modDocument
if ($class_key == 'modDocument') это исправил.
[[*views]] убрал, сразу время уменьшилось.
но это [[!+viewsTicket]] не отображается, может еще что поменять или весь плагин другой нужно?
Александр Мальцев
Александр Мальцев
Немного изменил плагин, теперь он предназначен для подсчёта любых ресурсов у которых есть TV поле views (указание какое TV-поле views имеет id осуществляется посредством переменной $id_tv).
<?php
if ($modx->event->name == 'OnLoadWebDocument') {
  // id дополнительного поля views
  $id_tv = 2;
  // получим ресурс
  $resource = $modx->resource;
  // проверить есть ли у ресурса (его шаблона) TV с указанным id
  $isTV = $modx->getObject('modTemplateVarTemplate',array(
    'tmplvarid' => $id_tv,
    'templateid' => $resource->get('template')
  ));
  // если TV нет, то завершаем работу
  if (!is_object($isTV)) {
    return;
  }
  // получим id ресурса
  $id = $resource->get('id');
  // получим url
  $url = $modx->makeUrl($id,'','',-1);

  // получим tv поле
  $tv_resource = $modx->getObject('modTemplateVarResource',array(
    'tmplvarid' => $id_tv,
    'contentid' => $id
  ));
  // получим id главной страницы
  $id_start = $modx->getOption('site_start');
  
  // если не существует COOKIE viewresource и viewmainresource
  if ((!isset($_COOKIE['viewresource']) && ($id!=$id_start)) || (!isset($_COOKIE['viewmainresource'])) && ($id==$id_start) ) {
    // если tv поле является объектом
    if (is_object($tv_resource)) {
      // получаем его значение
      $views_resource = $tv_resource->get('value');
      // увеличиваем количество просмотров на 1
      $views_resource++;
      // сохраняем количество просмотров в таблицу      
      $resource->setTVValue(2, $views_resource);
    } else {
      // устанавливаем количество просмотров равным 1
      $views_resource = 1;
      // сохраняем
      $resource->setTVValue(2,$views_resource);
    }      
    // отправка COOKIE
    if ($id==$id_start) {
      setcookie('viewmainresource', '1',time() + (86400 * 365),'/');       
    } else {
      setcookie('viewresource', '1',time() + (86400 * 365),'/'.$url);
    }
  } else {
    $views_resource = $tv_resource->get('value');
  }
  // устанавливаем плейсхолдер viewsTicket
  $modx->setPlaceholder('viewsResource', $views_resource);
}
Имя плейсхолдера изменил на более логичное viewsResource.
Демьян Золин
Демьян Золин
В плагин viewsCount поместил вышеприведенный код.
В чанк «Footer» добавил [[!+viewsResource]].
На странице не отображается, даже 0 и ноль не меняется в менеджере ресурса.
alex1
alex1
тоже самое сделал, вообще в ошибку 500 падает
Максим
Максим
if (!is_object($isTV)) {
    return;
  }
почему-то вот из-за этого дальше не отрабатывает ничего, но даже если убираем это вроде всё начинает работать, само поле views начинает нормально считаться но на вывод через viewsResource выводиться всегда главной страницы причём счётчик увеличивается каждый раз когда обновляешь страницу прмчём на рандомное значение от 2 до 5.
Не могу никак понять как в getPlaceholder вывести значение именно той новости что нужна
вывод идёт через pdotools, используется collection
		{$_modx->runSnippet('!pdoPage', [
			'tpl' => '@FILE chunks/header_news.tpl',
			'parents' => 2,
			'limit' => '1',
			'includeTVs' => 'imageMajor, headindex, views',
			'processTVs' => 1,
			'where' => '{"headindex":1}'

		])}
в чанке указываю так
{$_pls["tv.views"]} - выводится нужное значение из конкретной новост
{$modx->getPlaceholder('viewsResource')} - выводится значение той странице где расположен чанк вывода то есть главной страницы
[[+viewsResource]] - тоже самое что и предыдущее 
itchief.ru/assets/uploadify/1/b/8/1b88e5cd21a6af93445a6ac206dcd79a.png
Максим
Максим
В таком варианте хотя бы работает, но через viewsResource выводится значение текущей страницы, даже если поле шаблону не задано, приходится выводить через тв поле views что как я понимаю не очень хорошо
<?php
if ($modx->event->name == 'OnLoadWebDocument') {
    // id дополнительного поля views
    $id_tv = 6;
    // получим ресурс
    $resource = $modx->resource;
    $id = $resource->get('id');
    // проверить есть ли у ресурса (его шаблона) TV с указанным id
    $isTV = $modx->getObject('modTemplateVarTemplate',array(
        'tmplvarid' => $id_tv,
        'templateid' => $id
    ));

    // получим url
    $url = $modx->makeUrl($id,'','',-1);

    // получим tv поле
    $tv_resource = $modx->getObject('modTemplateVarResource',array(
        'tmplvarid' => $id_tv,
        'contentid' => $id
    ));
    
    // получим id главной страницы
    $id_start = $modx->getOption('site_start');

    // если не существует COOKIE viewresource и viewmainresource'
    if (!isset($_COOKIE['viewresource'])) {
        // если tv поле является объектом
        if (is_object($tv_resource)) {
            // получаем его значение
            $views_resource = $tv_resource->get('value');
            // увеличиваем количество просмотров на 1
            $views_resource++;
            // сохраняем количество просмотров в таблицу
            $tv_resource->set('value',$views_resource);
            $tv_resource->save();   
        } else {
            // устанавливаем количество просмотров равным 1
            $views_resource = 100;
            // сохраняем
            $tv_resource->$tv_resource->get('value');
            $tv_resource->save();   
        }
        // отправка COOKIE
        if ($id==$id_start) {
            setcookie('viewmainresource', '1',time() + (86400 * 365),'/');
        } else {
            setcookie('viewresource', '1',time() + (86400 * 365),'/'.$url);
        }
    } else {
        $views_resource = $tv_resource->get('value');
    }
    // устанавливаем плейсхолдер viewsTicket
    $modx->setPlaceholder('viewsResource', $views_resource);
}
Максим
Максим
Приношу извинения за выше написанное лучше удалите этот позор нафиг)
Всё работает то что вы написали

просто кроме $id_tv = 2; надо было поменять $resource->setTVValue(2,$views_resource); и тут
2 на нужный id tv