Есть следующий кейс:
Компонент news.list выводит элементы из инфоблока. У элементов инфоблока есть множественное свойство типа "Привязка к пользователю" под названием "Пользователи, которым понравилась новость". Задача: выводить логины пользователей, которым понравилась новость. Если пользователь авторизован, и он поставил "лайк", то выводить кнопку "Не нравится", если он не ставил "лайк", то выводить кнопку "Нравится". Ну т.е. типичный функционал оценки контента.
Мне пришло в голову несколько вариантов того, как можно организовать этот функционал в рамках штатного news.list и увязать его работу с управляемым кэшем компонента, некоторые варианты мне показались вполне приемлемыми, некоторые - не очень.
1) Добавить к news.list параметр "USER_ID" и скармливать ему id авторизованного пользователя. А в template уже работаем с $arParams["USER_ID"] . Серьёзный недостаток в том, что управляемый кэш зависит от id пользователя, что является очень нежелательной ситуацией, ибо для каждого авторизованного пользователя будет создаваться свой экземпляр кэша. Вес файлов кэша будет очень большой.
2) Добавить к news.list параметр "LIKED_NEWS_IDS" , который будет содержать id статей, лайкнутых авторизованным пользователем. Т.к. возможен такой вариант, когда несколько разных пользователей полайкали один и тот-же набор статей, то объём кэша должен быть меньше (ну по крайней мере не больше), чем в реализации, описанной в первом варианте. Вариант считаю неидеальным, зависимость управляемого кэша от id пользователя всё-равно присутствует, но на этот раз косвенная.
Нутром чую, что можно сделать как-то проще, но пока остановился на этом варианте.
Вот код:
news.php
Код метода HelperIblock::getLikedNews :
template.php :
result_modifier.php :
Метод HelperUsers::getAllRegisteredUsers :
js код обработчика кнопки лайка:
в обработчике меняем цвет и надпись кнопки , а так-же содержимое блока, в котором выведены логины пользователей, лайкнувших новость.
like.php. обработчик лайка, который вызываем ajax-ом :
Вот собственно и всё.
3) Ещё один вариант, который я спроектировал, но на практике не реализовывал.
Отказывается от параметра LIKED_NEWS_IDS и задействуем component_epilog. В нём через HelperIblock::getLikedNews получаем список id новостей, которые лайкнул текущий пользователь. Далее js-ом меняем цвет и надпись кнопок. js обработчик и ajax-обработчик лайка остаются такими-же, как в предыдущем варианте.
4) Посмотреть, как реализованы лайки в forum.topic.list
					Компонент news.list выводит элементы из инфоблока. У элементов инфоблока есть множественное свойство типа "Привязка к пользователю" под названием "Пользователи, которым понравилась новость". Задача: выводить логины пользователей, которым понравилась новость. Если пользователь авторизован, и он поставил "лайк", то выводить кнопку "Не нравится", если он не ставил "лайк", то выводить кнопку "Нравится". Ну т.е. типичный функционал оценки контента.
Мне пришло в голову несколько вариантов того, как можно организовать этот функционал в рамках штатного news.list и увязать его работу с управляемым кэшем компонента, некоторые варианты мне показались вполне приемлемыми, некоторые - не очень.
1) Добавить к news.list параметр "USER_ID" и скармливать ему id авторизованного пользователя. А в template уже работаем с $arParams["USER_ID"] . Серьёзный недостаток в том, что управляемый кэш зависит от id пользователя, что является очень нежелательной ситуацией, ибо для каждого авторизованного пользователя будет создаваться свой экземпляр кэша. Вес файлов кэша будет очень большой.
2) Добавить к news.list параметр "LIKED_NEWS_IDS" , который будет содержать id статей, лайкнутых авторизованным пользователем. Т.к. возможен такой вариант, когда несколько разных пользователей полайкали один и тот-же набор статей, то объём кэша должен быть меньше (ну по крайней мере не больше), чем в реализации, описанной в первом варианте. Вариант считаю неидеальным, зависимость управляемого кэша от id пользователя всё-равно присутствует, но на этот раз косвенная.
Нутром чую, что можно сделать как-то проще, но пока остановился на этом варианте.
Вот код:
news.php
<?
global $USER;
if ($USER->IsAuthorized()) {
    $arLikedNewsIDs = HelperIblock::getLikedNews($USER->GetID(), $arParams["IBLOCK_ID"]);
}else{
    $arLikedNewsIDs = array();
}
?>
<?$APPLICATION->IncludeComponent(
    "bitrix:news.list",
    "",
    Array(
        "IBLOCK_TYPE" => $arParams["IBLOCK_TYPE"],
        "IBLOCK_ID" => $arParams["IBLOCK_ID"],
        "NEWS_COUNT" => $arParams["NEWS_COUNT"],
        "LIKED_NEWS_IDS" => $arLikedNewsIDs, // id-шники новостей, лайкнутые авторизованным пользователем
        "SORT_BY1" => $arParams["SORT_BY1"],
        "SORT_ORDER1" => $arParams["SORT_ORDER1"],
        "SORT_BY2" => $arParams["SORT_BY2"],
        "SORT_ORDER2" => $arParams["SORT_ORDER2"],
        "FILTER_NAME" => $arParams["FILTER_NAME"],
        "FIELD_CODE" => $arParams["LIST_FIELD_CODE"],
        "PROPERTY_CODE" => $arParams["LIST_PROPERTY_CODE"],
        "CHECK_DATES" => $arParams["CHECK_DATES"],
        "IBLOCK_URL" => $arResult["FOLDER"].$arResult["URL_TEMPLATES"]["news"],
        "SECTION_URL" => $arResult["FOLDER"].$arResult["URL_TEMPLATES"]["section"],
        "DETAIL_URL" => $arResult["FOLDER"].$arResult["URL_TEMPLATES"]["detail"],
        "SEARCH_PAGE" => $arResult["FOLDER"].$arResult["URL_TEMPLATES"]["search"],
        "CACHE_TYPE" => $arParams["CACHE_TYPE"],
        "CACHE_TIME" => $arParams["CACHE_TIME"],
        "CACHE_FILTER" => $arParams["CACHE_FILTER"],
        "CACHE_GROUPS" => $arParams["CACHE_GROUPS"],
        "PREVIEW_TRUNCATE_LEN" => $arParams["PREVIEW_TRUNCATE_LEN"],
        "ACTIVE_DATE_FORMAT" => $arParams["LIST_ACTIVE_DATE_FORMAT"],
        "SET_TITLE" => $arParams["SET_TITLE"],
        "SET_BROWSER_TITLE" => "Y",
        "SET_META_KEYWORDS" => "Y",
        "SET_META_DESCRIPTION" => "Y",
        "MESSAGE_404" => $arParams["MESSAGE_404"],
        "SET_STATUS_404" => $arParams["SET_STATUS_404"],
        "SHOW_404" => $arParams["SHOW_404"],
        "FILE_404" => $arParams["FILE_404"],
        "SET_LAST_MODIFIED" => $arParams["SET_LAST_MODIFIED"],
        "INCLUDE_IBLOCK_INTO_CHAIN" => $arParams["INCLUDE_IBLOCK_INTO_CHAIN"],
        "ADD_SECTIONS_CHAIN" => "N",
        "HIDE_LINK_WHEN_NO_DETAIL" => $arParams["HIDE_LINK_WHEN_NO_DETAIL"],
        "PARENT_SECTION" => "",
        "PARENT_SECTION_CODE" => "",
        "INCLUDE_SUBSECTIONS" => "Y",
        "DISPLAY_DATE" => $arParams["DISPLAY_DATE"],
        "DISPLAY_NAME" => "Y",
        "DISPLAY_PICTURE" => $arParams["DISPLAY_PICTURE"],
        "DISPLAY_PREVIEW_TEXT" => $arParams["DISPLAY_PREVIEW_TEXT"],
        "MEDIA_PROPERTY" => $arParams["MEDIA_PROPERTY"],
        "SLIDER_PROPERTY" => $arParams["SLIDER_PROPERTY"],
        "PAGER_TEMPLATE" => $arParams["PAGER_TEMPLATE"],
        "DISPLAY_TOP_PAGER" => $arParams["DISPLAY_TOP_PAGER"],
        "DISPLAY_BOTTOM_PAGER" => $arParams["DISPLAY_BOTTOM_PAGER"],
        "PAGER_TITLE" => $arParams["PAGER_TITLE"],
        "PAGER_SHOW_ALWAYS" => $arParams["PAGER_SHOW_ALWAYS"],
        "PAGER_DESC_NUMBERING" => $arParams["PAGER_DESC_NUMBERING"],
        "PAGER_DESC_NUMBERING_CACHE_TIME" => $arParams["PAGER_DESC_NUMBERING_CACHE_TIME"],
        "PAGER_SHOW_ALL" => $arParams["PAGER_SHOW_ALL"],
        "PAGER_BASE_LINK_ENABLE" => $arParams["PAGER_BASE_LINK_ENABLE"],
        "PAGER_BASE_LINK" => $arParams["PAGER_BASE_LINK"],
        "PAGER_PARAMS_NAME" => $arParams["PAGER_PARAMS_NAME"],
        "USE_RATING" => $arParams["USE_RATING"],
        "DISPLAY_AS_RATING" => $arParams["DISPLAY_AS_RATING"],
        "MAX_VOTE" => $arParams["MAX_VOTE"],
        "VOTE_NAMES" => $arParams["VOTE_NAMES"],
        "USE_SHARE" => $arParams["LIST_USE_SHARE"],
        "SHARE_HIDE" => $arParams["SHARE_HIDE"],
        "SHARE_TEMPLATE" => $arParams["SHARE_TEMPLATE"],
        "SHARE_HANDLERS" => $arParams["SHARE_HANDLERS"],
        "SHARE_SHORTEN_URL_LOGIN" => $arParams["SHARE_SHORTEN_URL_LOGIN"],
        "SHARE_SHORTEN_URL_KEY" => $arParams["SHARE_SHORTEN_URL_KEY"],
        "TEMPLATE_THEME" => $arParams["TEMPLATE_THEME"],
    ),
    $component
);?> | 
Код метода HelperIblock::getLikedNews :
class HelperIblock
{
public static function getLikedNews($userID, $IBlockNewsID){
    if (!\Bitrix\Main\Loader::includeModule('iblock'))
        return;
    $arLikedNewsIDs = array();
    $cache = new CPHPCache();
    $cache_time = 86400;
    $cache_id = 'getLikedNews_' . SITE_ID . "_" . $userID . "_" . $IBlockNewsID;
    $cache_path = '/getLikedNews/';
    if ($cache_time > 0 && $cache->InitCache($cache_time, $cache_id, $cache_path)) {
        $res = $cache->GetVars();
        if (is_array($res["arLikedNewsIDs"]) &&
            (count($res["arLikedNewsIDs"]) > 0)) {
            echo "cached";
            $arLikedNewsIDs = $res["arLikedNewsIDs"];
        }
    }
    if (empty($arLikedNewsIDs)) {
  
        $arFilter = array(
            "IBLOCK_ID" => $IBlockNewsID,
            "PROPERTY_LIKED_USERS" => $userID
        );
        $rsElement = CIBlockElement::GetList(
            array(),
            $arFilter,
            false,
            false,
            array("ID", "PROPERTY_LIKED_USERS")
        );
        global $CACHE_MANAGER;
        $CACHE_MANAGER->StartTagCache($cache_path);
        while ($arElement = $rsElement->GetNext()) {
            $arLikedNewsIDs[] = $arElement["ID"];
            $CACHE_MANAGER->RegisterTag("LikedNewID_" . $arElement["ID"]);
        }
        $CACHE_MANAGER->EndTagCache();
        if ($cache_time > 0) {
            $cache->StartDataCache($cache_time, $cache_id, $cache_path);
            $cache->EndDataCache(array("arLikedNewsIDs" => $arLikedNewsIDs));
        }
    }
    return $arLikedNewsIDs;
}
}
 | 
template.php :
<div class="news-wrap__list ajax-list">
    <?foreach($arResult["ITEMS"] as $arItem):?>
        <?
        $this->AddEditAction($arItem['ID'], $arItem['EDIT_LINK'], CIBlock::GetArrayByID($arItem["IBLOCK_ID"], "ELEMENT_EDIT"));
        $this->AddDeleteAction($arItem['ID'], $arItem['DELETE_LINK'], CIBlock::GetArrayByID($arItem["IBLOCK_ID"], "ELEMENT_DELETE"), array("CONFIRM" => GetMessage('CT_BNL_ELEMENT_DELETE_CONFIRM')));
        $img = CFile::ResizeImageGet($arItem['PREVIEW_PICTURE'], array('width'=>375, 'height'=>370), BX_RESIZE_IMAGE_EXACT);
        ?>
        <div class="news-wrap__item js-news-wrap__item ajax-item"
             id="<?=$this->GetEditAreaId($arItem['ID']);?>">
            <a href="<?=$arItem['DETAIL_PAGE_URL']?>" class="news-item js-lazy"
               data-src="<?=$img['src']?>">
                <div class="news-item__content-wrap">
                    <div class="news-item__text-wrap">
                        <h4 class="news-item__title"><?=$arItem['NAME']?></h4>
                        <h5 class="news-item__subtitle"><?=$arItem['PROPERTIES']['SUBTITLE']['VALUE']?></h5>
                        <p class="news-item__date"><?=strtolower($arItem['DISPLAY_ACTIVE_FROM'])?></p>
                        <p class="news-item__desc js-news-slider-desc">
                            <?=$arItem['~PREVIEW_TEXT']?>
                        </p>
                    </div>
                    <button class="news-item__button">подробнее</button>
                </div>
            </a>
            <?
            if(!empty($arParams["LIKED_NEWS_IDS"])) {
                $arButtonData = array(
                    "LABEL" => GetMessage("LABEL_LIKE"),
                    "COLOR" => "green"
                );
                if (in_array($arItem['ID'], $arParams["LIKED_NEWS_IDS"])) {
                    $arButtonData = array(
                        "LABEL" => GetMessage("LABEL_DISLIKE"),
                        "COLOR" => "red"
                    );
                }
                ?>
                <div class="news-item__line">
                    <div class="news-item__input-holder">
                        <button class="btn btn_<?= $arButtonData["COLOR"]; ?>
                        btn_block send-btn js-like"
                                data-new-id="<?= $arItem['ID']; ?>"
                                data-iblock-id="<?= $arItem['IBLOCK_ID']; ?>"
                                data-like-label="<?=GetMessage("LABEL_LIKE");?>"
                                data-dislike-label="<?=GetMessage("LABEL_DISLIKE");?>"
                                data-handler-path="<?= $templateFolder . "/like.php"; ?>">
                            <?= $arButtonData["LABEL"]; ?>
                        </button>
                    </div>
                </div>
                <?
            }
            ?>
            <div class="news-item__line js-news-item__line">
                <?=$arItem["PROPERTIES"]["LIKED_USERS"]["VALUE_LOGINS_STR"];?>
            </div>
        </div>
    <?endforeach;?>
</div>
<?=$arResult['NAV_STRING']?>
 | 
result_modifier.php :
<?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
$arAllRegisteredUsersLogins = array();
$arAllRegisteredUsersLogins = HelperUsers::getAllRegisteredUsers(); // получаем логины всех зарегистрированных пользователей
foreach($arResult["ITEMS"] as &$arItem){
    $arLikedUsersLogins = array();
    sort($arItem["PROPERTIES"]["LIKED_USERS"]["VALUE"]);
    foreach($arItem["PROPERTIES"]["LIKED_USERS"]["VALUE"] as $intLikedUserID){
        $arLikedUsersLogins[] = $arAllRegisteredUsersLogins[$intLikedUserID];
    }
    $arItem["PROPERTIES"]["LIKED_USERS"]["VALUE_LOGINS_STR"] = implode(", ", $arLikedUsersLogins);
}
 | 
Метод HelperUsers::getAllRegisteredUsers :
class HelperUsers
{
    /**
     * @return mixed
     */
    public static function getAllRegisteredUsers()
    {
        $arAllRegisteredUsersLogins = array();
        $cache = new CPHPCache();
        $cache_time = 86400;
        $cache_id = 'arAllRegisteredUsersLogins_' . SITE_ID;
        $cache_path = '/arAllRegisteredUsersLogins/';
        if ($cache_time > 0 && $cache->InitCache($cache_time, $cache_id, $cache_path)) {
            $res = $cache->GetVars();
            if (is_array($res["arAllRegisteredUsersLogins"]) &&
                (count($res["arAllRegisteredUsersLogins"]) > 0))
                $arAllRegisteredUsersLogins = $res["arAllRegisteredUsersLogins"];
        }
        if (empty($arAllRegisteredUsersLogins)) {
            $order = array('sort' => 'asc');
            $tmp = 'sort';
            $rsUsers = CUser::GetList($order, $tmp);
            global $CACHE_MANAGER;
            $CACHE_MANAGER->StartTagCache($cache_path);
            while ($arrUser = $rsUsers->GetNext()) {
                $arAllRegisteredUsersLogins[$arrUser["ID"]] = $arrUser["LOGIN"];
                $CACHE_MANAGER->RegisterTag("RegisteredUser_" . $arrUser["ID"] . "_" . $arrUser["LOGIN"]);
            }
            $CACHE_MANAGER->EndTagCache();
            if ($cache_time > 0) {
                $cache->StartDataCache($cache_time, $cache_id, $cache_path);
                $cache->EndDataCache(array("arAllRegisteredUsersLogins" => $arAllRegisteredUsersLogins));
            }
        }
        return $arAllRegisteredUsersLogins;
    }
} | 
js код обработчика кнопки лайка:
$(function () {
    (function () {
        $(document).on("click", ".js-like", function () {
            var $button = $(this);
            var $parent = $button.closest(".js-news-wrap__item");
            var $blockUsers = $parent.find(".js-news-item__line");
            $.post($button.data("handler-path"), { newId: $button.data("new-id"), iblockId: $button.data("iblock-id") }, function (data) {
                if (data.content){ $blockUsers.html(data.content); }
                if (data.action === "like") {
                    $button.addClass("btn_red");
                    $button.removeClass("btn_green");
                    $button.html($button.data("dislike-label"));
                } else if (data.action === "dislike") {
                    $button.removeClass("btn_red");
                    $button.addClass("btn_green");
                    $button.html($button.data("like-label"));
                }
            }, "json");
        });
    })();
}); | 
like.php. обработчик лайка, который вызываем ajax-ом :
<?require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
CModule::IncludeModule('iblock');
$newId = intVal(htmlspecialchars($_POST['newId']));
$iblockId = intVal(htmlspecialchars($_POST['iblockId']));
if(empty($newId)) die();
if(empty($iblockId)) die();
global $USER;
if ($USER->IsAuthorized()) {
    $arAllRegisteredUsersLogins = array();
    $arAllRegisteredUsersLogins = HelperUsers::getAllRegisteredUsers();
    $arFilter = array(
        "IBLOCK_ID" => $iblockId,
        "ID" => $newId
    );
    $rsElement = CIBlockElement::GetList(
        array(),
        $arFilter,
        false,
        false,
        array("ID", "IBLOCK_ID", "PROPERTY_LIKED_USERS")
    );
    $PROPERTY_LIKED_USERS = array();
    while ($arElement = $rsElement->GetNext()) {
        /** PROPERTY_LIKED_USERS - это множественное свойство, поэтому вместо if
         * используется while */
        $PROPERTY_LIKED_USERS[] = $arElement["PROPERTY_LIKED_USERS_VALUE"];
    }
    sort($PROPERTY_LIKED_USERS);
    
    $strActionType = "like";
    if (in_array($USER->GetID(), $PROPERTY_LIKED_USERS)) {
        $PROPERTY_LIKED_USERS = array_diff($PROPERTY_LIKED_USERS, array($USER->GetID()));
        $strActionType = "dislike";
    } else {
        $PROPERTY_LIKED_USERS = array_merge($PROPERTY_LIKED_USERS, array($USER->GetID()));
    }
    sort($PROPERTY_LIKED_USERS);
    CIBlockElement::SetPropertyValueCode($newId, "LIKED_USERS", $PROPERTY_LIKED_USERS);
    if (defined('BX_COMP_MANAGED_CACHE')) {
        $GLOBALS['CACHE_MANAGER']->ClearByTag('iblock_id_' . $iblockId);
        $GLOBALS['CACHE_MANAGER']->ClearByTag('LikedNewID_' . $newId); // подозреваю, что эта строка здесь избыточна, но не стал проверять, оставил на всякий случай. 
//Сброса кэша по стандартному тэгу iblock_id_ должно быть вполне достаточно.
    }
    $arLikedUsersLogins = array();
    foreach ($PROPERTY_LIKED_USERS as $intLikedUserID) {
        $arLikedUsersLogins[] = $arAllRegisteredUsersLogins[$intLikedUserID];
    }
    $VALUE_LOGINS_STR = implode(", ", $arLikedUsersLogins);
    $result = array(
        'action' => $strActionType,
        'content' => $VALUE_LOGINS_STR
    );
    echo json_encode($result);
}else{
    die();
}
 | 
Вот собственно и всё.
3) Ещё один вариант, который я спроектировал, но на практике не реализовывал.
Отказывается от параметра LIKED_NEWS_IDS и задействуем component_epilog. В нём через HelperIblock::getLikedNews получаем список id новостей, которые лайкнул текущий пользователь. Далее js-ом меняем цвет и надпись кнопок. js обработчик и ajax-обработчик лайка остаются такими-же, как в предыдущем варианте.
4) Посмотреть, как реализованы лайки в forum.topic.list