Модуль Лайки, звезды, рейтинги, оценки (devnseo.liker) — это универсальная система реакций (лайков, оценок, звёзд, эмодзи) для сайтов на 1С-Битрикс. Модуль позволяет добавлять интерактивные виджеты для пользователей: голосование за контент, рейтинг товаров, звездочки, оценка статей и другое.
Ключевые возможности:
like, stars, thumbs, emoji. Можно расширить новыми типами.⚠️ Внимание!
Модуль сделан на основе моего js-модуля liker (https://github.com/qujs-dev/liker). Поэтому подключение возможно только через qu.js (https://github.com/qujs-dev/core).
Предварительно необходимо скачать базовый модуль для 1С-Битрикс Qu расширенная загрузка ассетов async/defer/media
Стандартная установка через Маркетплейс:
Важно: модуль требует установленного модуля devnseo.qu (Qu расширенная загрузка ассетов async/defer/media). При установке это будет проверено.
Перейдите в «Сервисы» → «Лайки, звезды, рейтинги, оценки» → вкладка «Настройки».
Доступные опции (для каждого сайта отдельно):
| Параметр | Описание |
|---|---|
allow_guests |
Разрешить голосование гостям (неавторизованным пользователям). |
guest_check_ip |
Проверять уникальность гостя по IP-адресу. Если выключено, используется токен. |
allow_container_create |
Разрешить создание контейнеров при голосовании. |
allowed_target_keys |
Список разрешённых TARGET_KEY (через запятую или с новой строки). Если поле пустое – ограничение не действует. |
debug_enabled |
Включить отладочный лог (AddMessage2Log) для отладки работы модуля. |
⚠️ Внимание!
Определитесь с определением по ip (guest_check_ip) сразу, т.к. от этой настройки технически зависит голосование за лайки. Если активировать позже, то могут быть некоторые артефакты, связанные с тем что использовались разные схемы голосования.
Модуль поддерживает четыре типа реакций "из коробки". Каждый тип определяет логику агрегации, формат ответа и способ отображения.
| Тип | Код | Описание | Поля ответа |
|---|---|---|---|
| Лайк | like |
Классический лайк (сердечко). Пользователь может поставить или убрать лайк. | total, total_likes, user_value |
| Звёзды | stars |
Рейтинг по звёздам (1–5, настраиваемый максимум). Поддерживает распределение голосов. | avg_rating, max_stars, stats, stars_distribution, stars_percents |
| Пальцы | thumbs |
Голосование «палец вверх» / «палец вниз». Значения: +1 и -1. | total_likes, total_dislikes, avg_rating, score_class |
| Эмодзи | emoji |
Выбор одного из нескольких эмодзи (по умолчанию 5). Хранит распределение. |
stats, emoji_percents, user_value |
Типы реакций можно расширить и добавить свои.
Компонент для быстрого вывода виджета реакции на странице.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
SITE_ID |
string | текущий сайт | Идентификатор сайта |
TARGET_KEY |
string | обязательно | Тип объекта (например, catalog_product, blog_post) |
TARGET_ID |
string | обязательно | Идентификатор объекта |
TYPE |
string | like |
Тип реакции: like, stars, thumbs, emoji |
TAG |
string | '' |
Дополнительный тег для разграничения контекстов |
THEME |
string | default |
Тема оформления (может использоваться в шаблоне) |
ALLOW_GUESTS |
boolean | null | Разрешить гостям голосовать для этого конкретного контейнера (переопределяет глобальную настройку) |
ACTIVE |
boolean | true | Активен ли контейнер (если false – виджет больше не доступен для голосования) |
MAX_STARS |
int | 5 | Максимальное количество звёзд (для типа stars) |
MAX_EMOJI |
int | 5 | Максимальное количество эмодзи (для типа emoji) |
LEXICON_JSON |
string | '' |
JSON-объект с переопределениями текстов для уведомлений |
<?php $APPLICATION->IncludeComponent(
"devnseo:liker",
"stars",
[
"TARGET_KEY" => "catalog_product",
"TARGET_ID" => $arResult["ID"],
"TYPE" => "stars",
"MAX_STARS" => 7,
"ALLOW_GUESTS" => null
]
); ?>
Компонент рендерит HTML-обёртку с атрибутами data-qu-liker-*, которая затем инициализируется JS-скриптом модуля. В зависимости от шаблона могут выводиться различные элементы: кнопки, звёзды, эмодзи, виджет со статистикой.
Поддерживаются пользовательские шаблоны – скопируйте папку /bitrix/components/devnseo/liker/templates/.default в /local/templates/ваш_шаблон/components/devnseo/liker/ и модифицируйте template.php и view.php. Разделение на template и view необходимо для того, чтобы не дублировать верстку на композитных страницах, где необходимо загружать большие коллекции объектов.
Чтобы на одном сайте было много разных рейтингов (товары, статьи, комментарии, страницы) и они не путались между собой, модуль создаёт для каждого объекта уникальный ключ. Этот ключ строится из комбинации ваших настроек.
Принцип простой: если вы измените хотя бы один из перечисленных ниже параметров – создастся новый контейнер для голосования, а старые голоса останутся в старом.
SHA1( SITE_ID + TARGET_KEY + TARGET_ID + TYPE + TAG )
SHA1 – это хеш-функция, которая превращает строку в уникальный идентификатор.
| Параметр | Что задаёт | Пример |
|---|---|---|
SITE_ID |
Сайт, на котором работает виджет. Голоса с разных сайтов не смешиваются. | s1, s2 |
TARGET_KEY |
Тип объекта, к которому относится голосование. Это как категория. | catalog_product, blog_post, comment, page |
TARGET_ID |
Конкретный объект внутри типа. Это может быть ID товара, символьный код или даже URL страницы. | 123, product-456, /about/ |
TYPE |
Тип реакции: like, stars, thumbs, emoji. |
like, stars |
TAG |
Дополнительный тег для разграничения контекстов. Например, чтобы отделить голоса из мобильной версии от десктопной. | mobile, sidebar, main |
⚠️ Важно!
TARGET_KEY + TARGET_ID - основная связка. Категория объектов TARGET_KEY, а в нее вложены объекты TARGET_ID, которые должны быть уникальны в рамках категории. Например категория TARGET_KEY:
pagesи внутри нее TARGET_ID: с урлами страниц. Обратите внимание что урлы которые отличаются регистром или даже одним символом — будут разными объектами, лучше получать например:$APPLICATION->GetCurPage(). Там где возможно привязаться к инфоблоку — лучше привязываться к числовым id. При смене урл — это будет уже другой контейнер! Так же если ваша страница доступна по разным адресам — это разные сущности!
Вот как могут выглядеть разные контейнеры для одного и того же товара:
// Рейтинг товара 123 через звёзды
SITE_ID = s1
TARGET_KEY = catalog_product
TARGET_ID = 123
TYPE = stars
TAG =
// → создаётся контейнер для звёзд товара 123
// Лайки для этого же товара
SITE_ID = s1
TARGET_KEY = catalog_product
TARGET_ID = 123
TYPE = like
TAG =
// → создаётся другой контейнер (уже для лайков)
// А это – отдельный тег для того же товара для создания списка избранного у пользователя
SITE_ID = s1
TARGET_KEY = catalog_product
TARGET_ID = 123
TYPE = like
TAG = favorite
// → третий контейнер, потому что тег другой
// Рейтинг страницы /about/service/ через звёзды
SITE_ID = s1
TARGET_KEY = pages
TARGET_ID = /about/service/
TYPE = stars
TAG =
// → создаётся контейнер для страницы /about/service/
catalog_product и Catalog_Product – это разные ключи.TARGET_KEY и TAG лучше использовать только латинские буквы, цифры, подчёркивания и дефисы.TARGET_KEY использовать – просто придумайте свой (например, my_object). Главное, чтобы он был осмысленным и не менялся в будущем.SITE_ID остаётся тем же – голоса сохранятся.Модуль использует три независимых механизма синхронизации, каждый из которых решает свою задачу. Все они работают автоматически и не требуют ручного вмешательства.
| Механизм | Компонент | Когда срабатывает | Что делает |
|---|---|---|---|
| Голосование | liker.js(библиотека qu.js) |
При клике на виджет | Отправляет запрос на vote.php, обновляет конкретный виджет и сохраняет состояние в localStorage. |
| Пакетная синхронизация | liker.sync.js(отдельный скрипт) |
При загрузке страницы (один раз) | Собирает все виджеты с data-qu-liker-wrapper-cached, отправляет один запрос на syncBatch и обновляет их. |
| Смена авторизации | Inline-скрипт с idliker-auth-events-inline |
При входе или выходе пользователя | Отслеживает изменение BX.message.USER_ID и вызывает Qu.Liker.syncFromServer() для обновления всех виджетов. |
liker.js
Это основная библиотека (https://github.com/qujs-dev/liker), которая подключается по умолчанию, если компонент присутствует на странице.
При клике на виджет отправляется AJAX-запрос на /bitrix/tools/devnseo.liker/vote.php. Сервер обрабатывает голос, обновляет контейнер и возвращает актуальные данные.
Виджет обновляет счётчики, состояние (user_value) и сохраняет данные в localStorage.
liker.sync.js
Этот скрипт подключается отдельно через Assets::include('liker-init-sync') и используется только для виджетов, выведенных через адаптер (с атрибутом data-qu-liker-wrapper-cached).
[data-qu-liker-wrapper-cached] [data-qu-liker].site_id, target_id, target_key, type, tag)./bitrix/tools/devnseo.liker/liker.php?action=syncBatch с массивом контекстов.qu:Liker:adapter:sync на window с данными { items, grouped, timestamp }.qu:Liker:adapter:sync// Через Que
Que(function (Qu, detail) {
console.log('Синхронизация завершена', detail);
}, 'qu:Liker:adapter:sync');
Важно: событие генерируется только при успешной синхронизации.
liker-auth-events
Это небольшой inline-скрипт, который регистрируется в /lib/Handler.php ассетом liker-auth-events
и подключается автоматически как зависимость liker-init-js.
Он подписывается на изменение BX.message.USER_ID через localStorage.
BX.message.USER_ID меняется с 0 на ID пользователя.0.Qu.Liker.syncFromServer().Если требуется принудительно обновить все виджеты на странице:
Qu.Liker.syncFromServer();
liker.sync.js нужен только для виджетов через адаптер. Если используете обычный компонент devnseo:liker – этот скрипт не нужен.liker-auth-events, который подключается вместе с liker-init-js.
Все клиентские файлы модуля (JS, CSS, inline-скрипты) управляются через систему ассетов devnseo.qu.
Это позволяет переопределить любой ассет без изменения кода модуля — достаточно создать файл-оверрайд в проекте.
OnQuAssetsBuild./local/php_interface/qu/common/ — для всех сайтов./local/php_interface/qu/SITE_ID/ — для конкретного сайта (высший приоритет).Важно: оверрайд полностью заменяет конфиг ассета.
В модуле Liker CSS подключён как инлайн-стили в ассете liker-init-css (файл /lib/Handler.php).
Чтобы заменить их на свои стили:
/local/php_interface/qu/common/ (или /local/php_interface/qu/SITE_ID/ для конкретного сайта).liker-init-css.php:<?php
// /local/php_interface/qu/common/liker-init-css.php
return [
'aliases' => ['latest' => '1.0'],
'versions' => [
'1.0' => [
'files' => [
'liker-css-inline' => [
'type' => 'inline',
'inlineType' => 'css',
'content' => '
[data-qu-liker-wrapper] {
display: inline-flex;
}
.qu-liker-wrapper {
opacity: 1;
transition: opacity 0.3s ease;
}
.qu-liker-wrapper--loading {
opacity: 0;
pointer-events: none;
}
.qu-liker-wrapper--no-anim * {
transition: none !important;
animation: none !important;
}
@starting-style {
.qu-liker-wrapper {
opacity: 0;
}
}
',
'attrs' => [],
],
],
],
],
];
Теперь вместо стандартных стилей будут подключаться ваши.
Модуль генерирует события, на которые можно подписаться для расширения функциональности.
| Событие | Описание |
|---|---|
OnBuildReactionRegistry |
Позволяет зарегистрировать собственные типы реакций. |
OnBeforeVote |
Вызывается перед выполнением голосования. Можно отменить голосование, изменить контекст, актора или значение. |
OnAfterVote |
Вызывается после успешного голосования. Можно выполнить дополнительные действия (логирование, уведомления и т.д.). |
EventManager::getInstance()->addEventHandler(
'devnseo.liker',
'OnBeforeVote',
static function (Event $event) {
$context = $event->getParameter('context');
$actor = $event->getParameter('actor');
$value = $event->getParameter('value');
// Логируем для проверки
AddMessage2Log('[OnBeforeVote] targetKey=' . $context->targetKey . ', old value=' . $value);
// Пример модификации: если значение < 5, увеличиваем до 5
if ((int)$value < 5) {
$newValue = 5;
} else {
$newValue = (int)$value;
}
return new EventResult(
EventResult::SUCCESS,
[
'value' => $newValue,
]
);
}
);
При работе с большими списками (каталог товаров, новости, блог, комментарии) каждый элемент должен отображать свой виджет реакций. Если для каждого элемента делать отдельный запрос к БД – это приведёт к проблеме N+1 и сильной нагрузке на сервер, даже при использовании технологии композитный сайт, т.к. каждый вызов компонента будет создавать отдельный запрос. Модуль предоставляет адаптер \devNseo\Liker\Service\LikerViewAdapter, который позволяет загрузить данные для всех элементов на странице за один ajax и один SQL запрос, так же как делает это композитный сайт. Для адаптера необходимо вывести только верстку, без вызова компонента, это так же позволяет не создавать объекты на каждый вывод виджета, до их реальной инициализации (первого голоса).
| Метод | Описание |
|---|---|
build_product_map(array $targets, string $site_id): array |
Принимает массив целей (каждый элемент – массив с полями target_id, target_key, type, tag, theme)
и возвращает карту, где ключ – уникальный идентификатор цели, а значение – узел с данными контейнера и готовым ответом.
|
make_target_map_key(array $target): string |
Генерирует уникальный ключ для цели (используется как индекс в карте). |
build_empty_node(int $target_id, string $target_key, string $type, string $tag, string $theme): array |
Создаёт узел для цели, у которой ещё нет контейнера (голосов). Используется как fallback. |
build_view(array $node): array |
Преобразует узел в массив, готовый для передачи в шаблон (LIKER_STARS_VIEW и т.д.).
Включает все необходимые поля для рендеринга виджета.
|
resolve_template_view(string $template_name, string $file_name = 'view.php'): string |
Возвращает путь к файлу шаблона для указанного имени шаблона. Используется для подключения нужного view.php. |
result_modifier.php собираем цели и строим карты<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
use Bitrix\Main\Loader;
use devNseo\Liker\Service\LikerViewAdapter;
$component = $this->getComponent();
$arParams = $component->applyTemplateModifications();
if (!Loader::includeModule('devnseo.liker')) {
return;
}
$adapter = new LikerViewAdapter();
$allTargets = [];
foreach ($arResult['ITEMS'] as $item) {
$targetId = (int)($item['ID'] ?? 0);
if ($targetId <= 0) {
continue;
}
// Звёзды
$allTargets[] = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'stars',
'tag' => '',
'theme' => '',
];
// Лайки
$allTargets[] = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'like',
'tag' => '',
'theme' => '',
];
// Эмодзи
$allTargets[] = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'emoji',
'tag' => '',
'theme' => '',
];
// Thumbs
$allTargets[] = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'thumbs',
'tag' => '',
'theme' => '',
];
}
// Один запрос на все цели
$allMap = $adapter->build_product_map($allTargets, SITE_ID);
foreach ($arResult['ITEMS'] as &$item) {
$targetId = (int)($item['ID'] ?? 0);
if ($targetId <= 0) {
continue;
}
// Stars
$starsTarget = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'stars',
'tag' => '',
'theme' => '',
];
$starsKey = $adapter->make_target_map_key($starsTarget);
$starsNode = $allMap[$starsKey] ?? $adapter->build_empty_node(
$starsTarget['target_id'],
$starsTarget['target_key'],
$starsTarget['type'],
$starsTarget['tag'],
$starsTarget['theme']
);
$item['LIKER_STARS_NODE'] = $starsNode;
$item['LIKER_STARS_VIEW'] = $adapter->build_view($starsNode);
// Like
$likeTarget = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'like',
'tag' => '',
'theme' => '',
];
$likeKey = $adapter->make_target_map_key($likeTarget);
$likeNode = $allMap[$likeKey] ?? $adapter->build_empty_node(
$likeTarget['target_id'],
$likeTarget['target_key'],
$likeTarget['type'],
$likeTarget['tag'],
$likeTarget['theme']
);
/*
// lexicon лучше передавать на враппер коллекции элементов, чтобы не дублировать
$likeNode['lexicon'] = [
'like_success_message' => 'Добавлено в избранное!',
];
*/
$item['LIKER_LIKE_NODE'] = $likeNode;
$item['LIKER_LIKE_VIEW'] = $adapter->build_view($likeNode);
// Emoji
$emojiTarget = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'emoji',
'tag' => '',
'theme' => '',
];
$emojiKey = $adapter->make_target_map_key($emojiTarget);
$emojiNode = $allMap[$emojiKey] ?? $adapter->build_empty_node(
$emojiTarget['target_id'],
$emojiTarget['target_key'],
$emojiTarget['type'],
$emojiTarget['tag'],
$emojiTarget['theme']
);
$item['LIKER_EMOJI_NODE'] = $emojiNode;
$item['LIKER_EMOJI_VIEW'] = $adapter->build_view($emojiNode);
// Thumbs
$thumbsTarget = [
'target_id' => $targetId,
'target_key' => 'catalog_product',
'type' => 'thumbs',
'tag' => '',
'theme' => '',
];
$thumbsKey = $adapter->make_target_map_key($thumbsTarget);
$thumbsNode = $allMap[$thumbsKey] ?? $adapter->build_empty_node(
$thumbsTarget['target_id'],
$thumbsTarget['target_key'],
$thumbsTarget['type'],
$thumbsTarget['tag'],
$thumbsTarget['theme']
);
$item['LIKER_THUMBS_NODE'] = $thumbsNode;
$item['LIKER_THUMBS_VIEW'] = $adapter->build_view($thumbsNode);
}
unset($item);
template.php) выводим виджетВызываем не компонент, а просто выводим верстку из шаблона компонента, которую оборачиваем в контейнер с data-qu-liker-wrapper-cached.
<?php
use devNseo\Liker\Service\LikerViewAdapter;
// например при выводе карточки товара
$adapter = new LikerViewAdapter();
$starsView = is_array($item['LIKER_STARS_VIEW'] ?? null) ? $item['LIKER_STARS_VIEW'] : [];
$starsView['source'] = 'catalog_card';
$starsView['max_stars'] = 3; // обратите внимание что для того чтобы значение передалось на сервер при голосовании его надо установить на этот тип объекта через событие onBuildReactionContext
$starsTemplateFile = $adapter->resolve_template_view('stars_extended', 'view.php');
if ($starsView && $starsTemplateFile !== '') {
extract($starsView, EXTR_OVERWRITE);
?>
<div class="catalog-item-stars qu-liker-wrapper qu-liker-wrapper--loading" data-qu-liker-wrapper data-qu-liker-wrapper-cached>
<?php include $starsTemplateFile; ?>
</div>
<?php
}
?>
<hr>
<?php
$adapter = new LikerViewAdapter();
$likeView = is_array($item['LIKER_LIKE_VIEW'] ?? null) ? $item['LIKER_LIKE_VIEW'] : [];
$likeView['source'] = 'catalog_card';
$likeTemplateFile = $adapter->resolve_template_view('like', 'view.php');
if ($likeView && $likeTemplateFile !== '') {
extract($likeView, EXTR_OVERWRITE);
?>
<div class="catalog-item-like qu-liker-wrapper qu-liker-wrapper--loading" data-qu-liker-wrapper data-qu-liker-wrapper-cached>
<?php include $likeTemplateFile; ?>
</div>
<?php
}
?>
<hr>
<?php
$adapter = new LikerViewAdapter();
$emojiView = is_array($item['LIKER_EMOJI_VIEW'] ?? null) ? $item['LIKER_EMOJI_VIEW'] : [];
$emojiView['source'] = 'catalog_card';
$emojiTemplateFile = $adapter->resolve_template_view('emoji', 'view.php');
if ($emojiView && $emojiTemplateFile !== '') {
extract($emojiView, EXTR_OVERWRITE);
?>
<div class="catalog-item-emoji qu-liker-wrapper qu-liker-wrapper--loading"
data-qu-liker-wrapper
data-qu-liker-wrapper-cached>
<?php include $emojiTemplateFile; ?>
</div>
<?php
}
?>
<hr>
<?php
$adapter = new LikerViewAdapter();
$thumbsView = is_array($item['LIKER_THUMBS_VIEW'] ?? null) ? $item['LIKER_THUMBS_VIEW'] : [];
$thumbsView['source'] = 'catalog_card';
$thumbsTemplateFile = $adapter->resolve_template_view('thumbs', 'view.php');
if ($thumbsView && $thumbsTemplateFile !== '') {
extract($thumbsView, EXTR_OVERWRITE);
?>
<div class="catalog-item-thumbs qu-liker-wrapper qu-liker-wrapper--loading"
data-qu-liker-wrapper
data-qu-liker-wrapper-cached>
<?php include $thumbsTemplateFile; ?>
</div>
<?php
}
?>
В component_epilog.php добавляем:
use Bitrix\Main\Loader;
if (Loader::includeModule('devnseo.liker')) {
\devNseo\Qu\Assets::include('liker-init-sync');
}
Это подключит JS-скрипт только на той странице где это действительно необходимо, и, который обработает все виджеты data-qu-liker-wrapper-cached и синхронизирует их одним запросом.
resolve_template_view, который подтягивает стандартную верстку компонента.Итог: один SQL-запрос вместо N на каждый вызов компонента + один AJAX-запрос на всю страницу — оптимальная стратегия для масштабируемых проектов.
Событие onBuildReactionContext вызывается при каждом AJAX-запросе голосования
(в файле /bitrix/tools/devnseo.liker/vote.php) перед вызовом ReactionManager::vote().
Оно позволяет модифицировать $context->meta, и эти изменения влияют на контейнер
при каждом голосовании, а не только при создании.
vote.php вызывается событие, вы меняете $context->meta['max_stars'] (или другие поля).ReactionManager::vote($context, $actor, $value).vote() для каждого типа реакции вызывается метод applyToContainer() соответствующего класса.StarsReactionType, EmojiReactionType) используют $context->meta для обновления JSON_META контейнера:$context->meta['max_stars'] и обновляет максимальное количество звёзд.$context->meta['max_emoji'] и обновляет максимальное количество эмодзи.JSON_META только если они явно обработаны в applyToContainer() соответствующего типа. Если вы хотите добавить свои поля, вам нужно модифицировать код типа реакции, чтобы он их использовал (например, добавить чтение $context->meta['my_field'] в applyToContainer()).$container['JSON_META'] вызывается ContainerRepository::saveAggregateState(), которая сохраняет изменения в БД.Таким образом, изменения через событие применяются при каждом голосовании.
stars – max_stars (меняет максимальное количество звёзд).emoji – max_emoji (меняет максимальное количество эмодзи).allowGuests – отдельное свойство контекста (не meta), разрешающее или запрещающее голосование гостям для данного контейнера.$context->meta не будут сохранены, если они не обработаны в applyToContainer() соответствующего типа.
Важно: не все типы реакций используют meta для обновления контейнера. Например, LikeReactionType не использует meta в applyToContainer(), поэтому изменения через событие не повлияют на него.
lib/Reaction/StarsReactionType.php – метод applyToContainer():
$maxStars = (int)($context->meta['max_stars'] ?? $meta['max_stars'] ?? 5);
$meta = $this->normalizeMeta($meta, $maxStars);
$container['JSON_META'] = $meta;
lib/Reaction/EmojiReactionType.php – аналогично для max_emoji.
lib/Service/ReactionManager.php – вызывает $type->applyToContainer() для каждого типа.
lib/Service/ContainerRepository.php – метод saveAggregateState() сохраняет обновлённый JSON_META.
В /local/php_interface/init.php:
use devNseo\Liker\Service\ReactionContext;
EventManager::getInstance()->addEventHandler(
'devnseo.liker',
'onBuildReactionContext',
static function (Event $event) {
$context = $event->getParameter('context');
if (!$context instanceof ReactionContext) {
return;
}
$source = (string)($context->meta['source'] ?? '');
// Для звёзд в каталоге – максимум 3
if ($context->type === 'stars' && $source === 'catalog_card') {
$context->meta['max_stars'] = 3;
$context->allowGuests = 0; // и например передать параметр только для авторизрованных
}
// Для звёзд на детальной странице – максимум 10
if ($context->type === 'stars' && $source === 'product_detail') {
$context->meta['max_stars'] = 10;
}
// Для эмодзи в каталоге – максимум 6
if ($context->type === 'emoji' && $source === 'catalog_card') {
$context->meta['max_emoji'] = 6;
}
// Другие поля не сохранятся в JSON_META, если их не обработает тип реакции
// Чтобы добавить своё поле, нужно модифицировать applyToContainer() соответствующего типа.
}
);
max_stars или max_emoji для виджетов подключаемых через адаптер.Событие
onBuildReactionContextпозволяет динамически менять параметры контейнера при каждом голосовании, но только те, которые явно обрабатываются в методеapplyToContainer()конкретного типа реакции. Дляstarsиemojiэтоmax_starsиmax_emojiсоответственно. Для добавления своих полей нужно модифицировать соответствующий тип реакции.
Модуль позволяет не только использовать встроенные типы (like, stars, thumbs, emoji),
но и расширять их поведение или добавлять совершенно новые типы.
Все пользовательские классы и обработчики событий рекомендуется размещать в папке /local/php_interface/devnseo.liker/:
/local/php_interface/init.php – главный файл для регистрации событий./local/php_interface/devnseo.liker/LikeReactionTypeOverride.php – класс для переопределения LikeReactionType./local/php_interface/devnseo.liker/SmileReactionType.php – класс для нового типа «Смайлы».Создайте файл /local/php_interface/devnseo.liker/LikeReactionTypeOverride.php:
<?php
namespace Custom\devNseo\Liker;
use devNseo\Liker\Reaction\LikeReactionType;
use devNseo\Liker\Service\ReactionContext;
class LikeReactionTypeOverride extends LikeReactionType
{
public function applyToContainer(
array $container,
string $action,
$newValue,
$oldValue,
ReactionContext $context
): array {
// Инициализируем значения
$container['TOTAL_VOTES'] = (int)($container['TOTAL_VOTES'] ?? 0);
$container['TOTAL_LIKES'] = (int)($container['TOTAL_LIKES'] ?? 0);
if ($action === 'create') {
// Добавляем 2 лайка
$container['TOTAL_VOTES'] += 2;
$container['TOTAL_LIKES'] += 2;
} elseif ($action === 'delete') {
// Удаляем 2 лайка (но не меньше 0)
$container['TOTAL_VOTES'] = max(0, $container['TOTAL_VOTES'] - 2);
$container['TOTAL_LIKES'] = max(0, $container['TOTAL_LIKES'] - 2);
}
// Для like update не используется, но если понадобится – можно добавить
return $container;
}
}
Создайте файл /local/php_interface/devnseo.liker/SmileReactionType.php:
<?php
namespace Custom\devNseo\Liker;
use devNseo\Liker\Reaction\AbstractReactionType;
use devNseo\Liker\Service\ReactionContext;
class SmileReactionType extends AbstractReactionType
{
public function getCode(): string
{
return 'smile';
}
public function applyToContainer(
array $container,
string $action,
$newValue,
$oldValue,
ReactionContext $context
): array {
$meta = $this->extractMeta($container['JSON_META'] ?? []);
$stats = $meta['stats'] ?? ['smiles' => [1 => 0, 2 => 0, 3 => 0]];
if ($action === 'create') {
$stats['smiles'][(int)$newValue]++;
} elseif ($action === 'delete') {
$stats['smiles'][(int)$oldValue] = max(0, $stats['smiles'][(int)$oldValue] - 1);
} elseif ($action === 'update') {
$stats['smiles'][(int)$oldValue] = max(0, $stats['smiles'][(int)$oldValue] - 1);
$stats['smiles'][(int)$newValue]++;
}
$meta['stats'] = $stats;
$container['JSON_META'] = $meta;
$container['TOTAL_VOTES'] = array_sum($stats['smiles']);
$container['TOTAL_LIKES'] = $container['TOTAL_VOTES'];
return $container;
}
public function buildResponse(array $container, ?array $record = null): array
{
$data = parent::buildResponse($container, $record);
$meta = $this->extractMeta($container['JSON_META'] ?? []);
$stats = $meta['stats']['smiles'] ?? [1 => 0, 2 => 0, 3 => 0];
$totalVotes = (int)($container['TOTAL_VOTES'] ?? 0);
$percents = [];
foreach ($stats as $value => $count) {
$percents[$value] = $totalVotes > 0 ? (int)round(($count / $totalVotes) * 100) : 0;
}
$data['smiles_distribution'] = $stats;
$data['smiles_percents'] = $percents;
return $data;
}
public function recountFromRecords(array $container, array $records, ReactionContext $context): array
{
$stats = [1 => 0, 2 => 0, 3 => 0];
foreach ($records as $record) {
$value = (int)($record['VALUE'] ?? 0);
if (in_array($value, [1, 2, 3])) {
$stats[$value]++;
}
}
$container['JSON_META'] = ['stats' => ['smiles' => $stats]];
$container['TOTAL_VOTES'] = array_sum($stats);
$container['TOTAL_LIKES'] = $container['TOTAL_VOTES'];
return $container;
}
private function extractMeta($meta): array
{
if (is_string($meta)) {
$decoded = json_decode($meta, true);
return is_array($decoded) ? $decoded : [];
}
return is_array($meta) ? $meta : [];
}
}
В файле /local/php_interface/init.php добавьте:
EventManager::getInstance()->addEventHandler(
'devnseo.liker',
'OnBuildReactionRegistry',
static function (Event $event) {
$registry = $event->getParameter('registry');
require_once __DIR__ . '/devnseo.liker/LikeReactionTypeOverride.php';
require_once __DIR__ . '/devnseo.liker/SmileReactionType.php';
// Заменяем стандартный like на переопределённый
$registry->register(new Custom\devNseo\Liker\LikeReactionTypeOverride(), true);
// Добавляем новый тип «Смайлы»
$registry->register(new Custom\devNseo\Liker\SmileReactionType());
}
);
⚠️ Важно!
- При переопределении существующего типа вы полностью заменяете его логику. Убедитесь, что ваш класс корректно обрабатывает все случаи (создание, обновление, удаление, пересчёт).
- Если вы добавляете новый тип, укажите для него уникальный
code, который не совпадает со стандартными (like,stars,thumbs,emoji).- Для корректной работы в административном интерфейсе и в ответах виджетов ваш тип должен возвращать все необходимые поля в
buildResponse().- При использовании адаптера
LikerViewAdapterвам нужно будет доработать шаблоны для отображения нового типа, так как стандартные шаблоны рассчитаны на встроенные типы.
Модуль позволяет реализовать персональное избранное для пользователей с помощью механизма tag. Для этого используется обычный тип реакции (TYPE) like, но с тегом (TAG) например favorite, который отделяет избранное от обычных лайков и выберите шаблон компонента favorite.
И, например, укажите словарь (JSON) для компонента:
{
"like_success_title": "Добавлено в избранное!",
"like_success_message": "Товар добавлен в ваш список избранного.",
"like_delete_title": "Удалено из избранного",
"like_delete_message": "Товар удалён из списка избранного."
}
Или при вызове в шаблоне страницы/другого компонента.
<?php
$APPLICATION->IncludeComponent(
"devnseo:liker",
"favorite",
[
"TARGET_KEY" => "products",
"TARGET_ID" => $arResult["ID"],
"TYPE" => "like",
"TAG" => "favorite",
"LEXICON_JSON" => '{
"like_success_title": "Добавлено в избранное!",
"like_success_message": "Товар добавлен в ваш список избранного.",
"like_delete_title": "Удалено из избранного",
"like_delete_message": "Товар удалён из списка избранного."
}',
]
);
?>
На отдельной странице избранного нужно показать только те товары, которые пользователь добавил в избранное.
Для этого используется запрос к RecordTable по USER_ID и TAG = 'favorite'.
<?php
use devNseo\Liker\Model\RecordTable;
global $USER;
$userId = (int)$USER->GetID();
$records = RecordTable::getList([
'select' => ['CONTAINER_TARGET_ID' => 'CONTAINER.TARGET_ID'],
'filter' => [
'=USER_ID' => $userId,
'=CONTAINER.TAG' => 'favorite',
'=CONTAINER.TYPE' => 'like',
'=CONTAINER.SITE_ID' => SITE_ID,
],
'order' => ['ID' => 'DESC'], // ASC – сначала старые, DESC – сначала новые
])->fetchAll();
$favoriteIds = array_column($records, 'CONTAINER_TARGET_ID');
$GLOBALS['favoriteIds'] = $favoriteIds;
<?php
if (!empty($favoriteIds)) {
global $arrFilterFavorite;
$arrFilterFavorite = ['=ID' => $favoriteIds];
$APPLICATION->IncludeComponent(
"bitrix:catalog.section",
"", // шаблон
[
// Обязательный фильтр по ID
"FILTER_NAME" => "arrFilterFavorite",
"IBLOCK_ID" => 1, // ID инфоблока
"SECTION_ID" => 0, // 0 – все элементы (или ID раздела)
],
false
);
} else {
echo "У вас пока нет избранных товаров.
";
}
В result_modifier.php пробрасываем сортировку по id.
<?php
if (!empty($GLOBALS['favoriteIds'])) {
$orderMap = array_flip(array_map('intval', $GLOBALS['favoriteIds']));
usort($arResult['ITEMS'], function($a, $b) use ($orderMap) {
$idA = (int)$a['ID'];
$idB = (int)$b['ID'];
$posA = isset($orderMap[$idA]) ? $orderMap[$idA] : PHP_INT_MAX;
$posB = isset($orderMap[$idB]) ? $orderMap[$idB] : PHP_INT_MAX;
return $posA - $posB;
});
}
Все можно адаптировать под свой проект и под любой другой компонент. Создавайте уникальные TARGET_KEY для каждого типа объектов, а в идеале привязывайтесь к числовым id, что позволит избежать дублирования механизма голосований.
Параметр LEXICON_JSON — Словарь (JSON) позволяет переопределить тексты уведомлений, которые показываются пользователю при голосовании (успех, удаление, обновление, ошибки).
Это полезно для кастомизации языка интерфейса, создания собственных сценариев (например, избранное) или адаптации под разные типы реакций.
Где задаётся: в параметрах компонента devnseo:liker или в верстке parent элемента контейнера через атрибут data-qu-liker-lexicon.
Формат: JSON-объект, где ключи — строковые идентификаторы, значения — строки с текстом. Все ключи необязательны.
| Ключ | Описание | Значение по умолчанию | Действие |
|---|---|---|---|
like_success_title |
Заголовок уведомления при добавлении голоса | «Спасибо!» | create |
like_success_message |
Текст уведомления при добавлении голоса | «Ваш голос учтён.» | create |
like_delete_title |
Заголовок уведомления при удалении голоса | «Голос отменён!» | delete |
like_delete_message |
Текст уведомления при удалении голоса | «Вы отменили свой голос.» | delete |
like_update_title |
Заголовок уведомления при изменении голоса | «Голос изменён!» | update |
like_update_message |
Текст уведомления при изменении голоса | «Ваш голос изменён.» | update |
При возникновении ошибки используются соответствующие ключи. Если ключ не задан – используется значение по умолчанию.
| Ключ | Описание | Значение по умолчанию |
|---|---|---|
error_title_guest_voting_disabled |
Заголовок при отключении голосования для гостей | «Голосование недоступно» |
error_message_guest_voting_disabled |
Текст при отключении голосования для гостей | «Гостевое голосование отключено» |
error_title_guest_ip_required |
Заголовок при отсутствии IP у гостя | «Не удалось определить гостя» |
error_message_guest_ip_required |
Текст при отсутствии IP у гостя | «Для гостя не определён IP» |
error_title_guest_token_required |
Заголовок при отсутствии токена у гостя | «Не удалось определить гостя» |
error_message_guest_token_required |
Текст при отсутствии токена у гостя | «Для гостя не определён токен» |
error_title_voting_closed |
Заголовок при завершении голосования | «Голосование завершено» |
error_message_voting_closed |
Текст при завершении голосования | «Голосование для этого объекта завершено» |
error_title_target_key_not_allowed |
Заголовок при запрещённом TARGET_KEY | «Доступ запрещён» |
- Все ключи необязательны — если не указаны, используются стандартные тексты.
- Для кастомных типов реакций можно добавить свои ключи, обработав их в событиях или в шаблоне.
- Ключи чувствительны к регистру (используйте нижний регистр).
Вместо того чтобы дублировать одинаковые параметры (например, lexicon) для каждого виджета,
вы можете задать их один раз на родительском контейнере и передать дочерним виджетам через механизм наследования.
data-qu-liker-inherit.data-qu-liker-lexicon.Важно: наследование работает только для атрибутов data-qu-liker-*. Сам атрибут data-qu-liker-inherit не наследуется — его нужно ставить на каждом виджете.
<div data-qu-liker-lexicon='{"like_success_title":"Добавлено!","like_success_message":"Товар в избранном"}'>
<?php $APPLICATION->IncludeComponent(
"devnseo:liker",
"favorite",
[
"TARGET_KEY" => "catalog_product",
"TARGET_ID" => 123,
"TYPE" => "like",
"TAG" => "favorite",
"INHERIT" => "Y", // ← виджет унаследует lexicon от родителя
]
); ?>
<?php $APPLICATION->IncludeComponent(
"devnseo:liker",
"favorite",
[
"TARGET_KEY" => "catalog_product",
"TARGET_ID" => 456,
"TYPE" => "like",
"TAG" => "favorite",
"INHERIT" => "Y", // ← тоже унаследует
]
); ?>
</div>
// предпочтительно использовать в адаптерах
<?php
$adapter = new LikerViewAdapter();
$likeView = is_array($item['LIKER_LIKE_VIEW'] ?? null) ? $item['LIKER_LIKE_VIEW'] : [];
$likeView['source'] = 'catalog_card';
$likeView['inherit'] = true; // ← создаст аттрибут data-qu-liker-inherit
$likeTemplateFile = $adapter->resolve_template_view('like', 'view.php');
if ($likeView && $likeTemplateFile !== '') {
extract($likeView, EXTR_OVERWRITE);
?>
<div class="catalog-item-like qu-liker-wrapper qu-liker-wrapper--loading" data-qu-liker-wrapper data-qu-liker-wrapper-cached>
<?php include $likeTemplateFile; ?>
</div>
<?php
}
?>
⚠️ Внимание!
Это нужно в основном для использования в адаптерах, чтобы не дублировать lexicon для каждого элемента по отдельности.
lexicon – словарь текстовtheme – тема оформленияsource – источникdata-qu-liker-*Наследование работает для всех параметров, которые передаются через data-атрибуты. Это удобно для массового задания общих настроек на странице.
data-qu-liker-inherit должен быть установлен на самом виджете (data-qu-liker).<body>.Подробнее про html-js логику на странице https://qujs.ru/liker/.
\devNseo\Liker\Bootstrap\ModuleBootstrapFacadeФасад для доступа к основным сервисам модуля. Все методы статические.
getRegistry()Возвращает реестр типов реакций (ReactionTypeRegistry).
$registry = \devNseo\Liker\Bootstrap\ModuleBootstrapFacade::getRegistry();
$types = $registry->all(); // массив всех типовgetVotePolicy()Возвращает политику голосования (VotePolicy) – определяет, разрешено ли голосование гостям, проверка IP и т.д.
$policy = \devNseo\Liker\Bootstrap\ModuleBootstrapFacade::getVotePolicy();
getContainerRepository()Возвращает репозиторий контейнеров (ContainerRepository) – для работы с контейнерами голосования.
$repo = \devNseo\Liker\Bootstrap\ModuleBootstrapFacade::getContainerRepository();
$container = $repo->findOneById(123);
getReactionManager()Возвращает менеджер реакций (ReactionManager) – для выполнения голосования.
$manager = \devNseo\Liker\Bootstrap\ModuleBootstrapFacade::getReactionManager();
$result = $manager->vote($context, $actor, $value);
getRecountManager()Возвращает менеджер пересчёта (RecountManager) – для ручного пересчёта статистики контейнера.
$recount = \devNseo\Liker\Bootstrap\ModuleBootstrapFacade::getRecountManager();
$result = $recount->recountByContainerId(123);
\devNseo\Liker\Service\ReactionContextКонтекст голосования – идентифицирует объект, к которому относится реакция.
$context = new ReactionContext(
's1', // siteId
'catalog_product', // targetKey
'12345', // targetId
'like', // type (like, stars, thumbs, emoji)
'main', // tag (дополнительный идентификатор)
['max_stars' => 5] // meta-данные
);
\devNseo\Liker\Service\ReactionActorАктор голосования – пользователь или гость.
$actor = new ReactionActor(
$userId, // int|null – ID авторизованного пользователя
$_SERVER['REMOTE_ADDR'], // IP гостя
$guestToken // токен гостя из куки
);
\devNseo\Liker\Service\ReactionManagerОсновной класс для управления голосами.
vote()Проголосовать.
$result = $manager->vote($context, $actor, $value);
if ($result->isSuccess()) {
$data = $result->getData(); // содержит response, container, record, action
} else {
$errors = $result->getErrors();
}
removeVote()Отменить голосование.
$result = $manager->removeVote($context, $actor);
\devNseo\Liker\Service\ReactionRenderServiceСервис для получения состояния реакции (с кешированием).
build()Возвращает состояние контейнера и готовый ответ для рендеринга.
$renderService = new ReactionRenderService();
$config = new ReactionRenderConfig('s1', 'catalog_product', '123', 'like', '');
$result = $renderService->build($config, $actor);
if ($result->isSuccess()) {
$state = $result->getData();
// $state['container'] – данные контейнера
// $state['response'] – подготовленный ответ для виджета
}
\devNseo\Liker\Reaction\ReactionTypeRegistryРеестр типов реакций. Позволяет регистрировать новые типы и получать существующие.
$registry = new ReactionTypeRegistry();
$registry->registerDefaults(); // регистрирует like, stars, thumbs, emoji
// Регистрация кастомного типа
$registry->register(new MyCustomReactionType());
// Получение типа
$typeResult = $registry->require('like');
$type = $typeResult->getData()['type'];
\devNseo\Liker\Service\ContainerRepositoryРепозиторий для работы с контейнерами голосования.
$repo = new ContainerRepository();
// Найти контейнер по контексту
$container = $repo->findOneByContext($context);
// Получить или создать контейнер
$result = $repo->getOrCreate($context);
// Сохранить агрегированное состояние
$result = $repo->saveAggregateState($containerId, $fields);
Модуль предоставляет административные страницы для управления контейнерами и записями голосования, а также ручного пересчёта статистики.
Перейдите в «Сервисы» → «Лайки, звезды, рейтинги, оценки» → «Список лайков». Здесь отображаются все контейнеры с возможностью фильтрации по сайту, типу, target_key и другим полям.
Вы можете просмотреть детали контейнера, отредактировать его настройки (allow_guests, active, meta), а также выполнить пересчёт статистики.
На странице «Список голосов» перечислены все голоса с указанием контейнера, значения, пользователя (или IP/токен для гостей).
В процессе работы модуля статистика контейнера обновляется инкрементально:
Это сделано для производительности: не нужно каждый раз перебирать все записи, чтобы пересчитать рейтинг.
Кроме того, инкрементальный режим даёт гибкость администратору — вы можете вносить правки напрямую (добавлять недостающие голоса, переносить данные из других систем, исправлять ошибки), а статистика продолжит обновляться относительно текущего состояния.
Чтобы привести статистику контейнера в соответствие с фактическими данными, на странице его редактирования нажмите кнопку «Пересчитать из голосов».
Техническая поддержка бесплатных решений (а также помощь в установке/настройке) осуществляется на платной основе.
Поддержка платных решений:
Поддержка осуществляется: Пн-Пт с 18:00 до 21:00 по московскому времени (суббота, воскресенье и праздничные дни - выходные).
Время реагирования: в порядке живой очереди.
Обращаться по e-mail: info@devnseo.ru.
При обращении укажите: