Поиск других подходящих функций в модуле тоже не дал результата. Остается два пути. Первый – самостоятельно реализовать нужный нам метод. С точки зрения системного подхода, это более правильное решение, но... Здесь могут возникнуть проблемы обратной совместимости. Модуль довольно новый, мало ли что может измениться в недалеком будущем. Второй – получить необходимые данные, скомбинировав результаты нескольких стандартных методов, благо изучение исходных текстов модуля дает четкое представление, как это сделать. Вот и попробуем.
Общая идея выглядит так. Для объектов типа «пользователь» мы получаем списки идентификаторов пользователей-друзей и идентификаторов групп, для сущности «группа» – список идентификаторов пользователей-участников. Далее из списков идентификаторов пользователей исключаем тех, чьи записи не должны войти в «Ленту друзей». Аналогично обрабатываем список идентификаторов групп. Какие именно записи из блогов не должны включаться в ленты, мы выяснили при разборе свойств объектов «пользователь» и «группа». На основе созданных списков пользователей и групп составляем списки идентификаторов блогов, извлекаем необходимые записи и передаем их в шаблон для вывода на Web-странице, откуда был вызван компонент.
Для получения списка записей в блогах будем использовать функцию CBlogPost::GetList(). На первый взгляд, больше подходит функция CBlogUser::GetUserFriendsList(), которая специально предназначена для формирования списка сообщений друзей пользователя, но, к сожалению, она опирается на связи пользователей внутри модуля блогов, а не социальной сети, а это значит, что она нам не подходит. Функция же CBlogPost::GetList() возвращает список записей, соответствующих заданному фильтру, с возможностью сортировки и разбивки на страницы.
Включение в «Ленты» записей собственных блогов (для группы или пользователя) будем рассматривать как необязательное. Для ускорения обработки запросов и уменьшения нагрузки на сервер информацию, независящую от конкретного обратившегося посетителя, будем кэшировать. Итак, основные входящие параметры:
• $arParams['USER_ID'] – ID пользователя для построения ленты;
• $arParams['GROUP_ID'] – ID группы социальной сети для построения ленты;
• $arParams['BLOG_GROUP_ID'] – ID группы блогов, к которой принадлежат все блоги социальной сети;
• $arParams['INC_SELF_MESSAGES'] – включать ли в ленту сообщения из блога сущности.
Определение базовых прав пользователя, которые заодно будут использоваться как дополнительный идентификатор кэша (см. листинг 1). Обратим внимание на структуру $arResult['CURRENT_ACCESS']. В ней задаются права, доступные текущему пользователю по умолчанию. Далее получаем его идентификатор и определяем реальное состояние прав (листинг 2).
// Определим права текущего пользователя (который в данный
// момент смотрит "Ленту друзей")
$arResult = array();
// $arResult['ENTITY_TYPE'] – тип ленты,
// U – "лента пользователя", G – "лента группы"
$arResult['ENTITY_TYPE'] = $arParams['USER_ID'] >
0 ? 'U' : 'G';
// $arResult['ENTITY_ID'] – ID пользователя или группы
// (в зависимости от типа ленты)
$arResult['ENTITY_ID'] = $arResult['ENTITY_TYPE'] ==
'U' ? $arParams['USER_ID'] : $arParams['GROUP_ID'];
$arResult['CURRENT_ACCESS'] = array(
'canViewUserFriends' => false, //можно ли смотреть
// друзей пользователя
'canViewUserGroups' => false, // можно ли смотреть
// группы пользователя
'canViewUserSelfMessages' => false, // можно ли смотреть
// собственные записи
// блога пользователя
'canViewGroup' => false, // видима ли группа
'canViewGroupSelfMessages' => false // можно ли смотреть
// собственные записи
// блога группы
);
$isModuleAdmin = CSocNetUser::IsCurrentUserModuleAdmin();
$currentUserID = $GLOBALS['USER']->GetID();
if($arResult['ENTITY_TYPE'] == 'G') {
// для ленты групп проверим право на доступ к ней
$arResult['GROUP_INFO'] = CSocNetGroup::GetByID($arResult['ENTITY_ID']);
$arCurrentUserPerms = CSocNetUserToGroup::InitUserPerms($currentUserID, $arResult['GROUP_INFO'], $isModuleAdmin);
$arResult['CURRENT_ACCESS']['canViewGroup'] = $arCurrentUserPerms['UserCanViewGroup'];
unset($arCurrentUserPerms);
if($arParams['INC_SELF_MESSAGES'] && $arResult['CURRENT_ACCESS']['canViewGroup']) {
$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages'] = CSocNetFeaturesPerms::CanPerformOperation($currentUserID,
SONET_ENTITY_GROUP, $arResult['ENTITY_ID'], 'blog', 'view_post', $isModuleAdmin);
}
} else {
//для ленты пользователя проверим доступ к ней текущего пользователя
$arCurrentUserPerms = CSocNetUserPerms::InitUserPerms($currentUserID, $arResult['ENTITY_ID'], $isModuleAdmin);
$arResult['CURRENT_ACCESS']['canViewUserFriends'] = $arCurrentUserPerms['Operations']['viewfriends'];
$arResult['CURRENT_ACCESS']['canViewUserGroups'] = $arCurrentUserPerms['Operations']['viewgroups'];
if($arParams['INC_SELF_MESSAGES']) {
// если не нужно включать в ленту сообщения из своего блога, то и проверять ID пользователя не будем
// (экономим на количестве кэш-файлов)
$arResult['CURRENT_ACCESS']['canViewUserSelfMessages'] = $currentUserID == $arResult['ENTITY_ID'];
}
unset($arCurrentUserPerms);
}
unset($currentUserID, $isModuleAdmin);
Значение $arResult['CURRENT_ACCESS'] и будет дополнительным идентификатором кэша:
if($this->StartResultCache(false, array($arNavigation,
$arResult['CURRENT_ACCESS']), $cachePath)) {
//код компонента
//подключение шаблона сохранения результатов в кэш.
$this->IncludeComponentTemplate();
}
где $arNavigation – массив управляющих параметров для постраничной навигации, $cachePath – путь для хранения кэш-файла. Определение идентификаторов друзей и групп для получения по ним ID блогов (исходный текст несколько сокращен для обозримости, листинг 3).
// массив идентификаторов пользователей, из блогов которых
// будут выбираться записи
$arEntityUsersID = array();
// массив идентификаторов групп, из блогов которых будут
// выбираться записи
$arEntityGroupsID = array();
if($arResult['ENTITY_TYPE'] == 'U') {
//лента пользователя
//можно ли смотреть друзей для данного пользователя,
// доступны ли вообще блоги и друзья
$getFriends = false;
if($arResult['CURRENT_ACCESS']['canViewUserFriends']) {
$getFriends = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_USER, 'blog') &&
CSocNetUser::IsFriendsAllowed();
}
// можно ли смотреть группы для данного пользователя
// и доступны ли вообще блоги для групп
if($arResult['CURRENT_ACCESS']['canViewUserGroups']) {
$getGroups = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_GROUP, 'blog');
}
Определим ID друзей пользователя (листинг 4).
if($getFriends) {
$arOrderUR = array();
$arFilterUR = array(
'RELATION' => SONET_RELATIONS_FRIEND,
'USER_ID' => $arResult['ENTITY_ID'],
);
$arGroupByUR = false;
$arNavigationUR = false;
$arSelectFieldsUR = array(
'FIRST_USER_ID',
'SECOND_USER_ID'
);
$rsItems = CSocNetUserRelations::GetList($arOrderUR,
$arFilterUR, $arGroupByUR, $arNavigationUR,
$arSelectFieldsUR);
while($arItem = $rsItems->Fetch())
{
$id_ = $arItem['FIRST_USER_ID'] ==
$arResult['ENTITY_ID'] ?
intval($arItem['SECOND_USER_ID']) :
intval($arItem['FIRST_USER_ID']);
$arEntityUsersID[$id_] = $id_;
}
unset($rsItems, $id_, $arOrderUR, $arGroupByUR,
$arNavigationUR, $arSelectFieldsUR);
}
Код исключения «закрытых» пользователей вынесен в конец модуля (он общий для двух типов лент). Схема довольно проста: добавим ID владельца в массив $arEntityUsersID, если включен режим вывода сообщений из блога владельца ленты. Здесь же важно заполнить записью массив $arEntityUsersID на случай, если пользователь запретил смотреть своих друзей (листинг 5).
if($arParams['INC_SELF_MESSAGES']) {
$arEntityUsersID[$arResult['ENTITY_ID']] =
$arResult['ENTITY_ID'];
}
// Определим ID видимых и НЕзакрытых групп пользователя
if($getGroups)
{
$arOrderGR = array();
$arFilterGR = array(
'USER_ID' => $arResult['ENTITY_ID'],
'<=ROLE' => SONET_ROLES_USER,
'GROUP_SITE_ID' => SITE_ID,
'GROUP_ACTIVE' => 'Y',
'GROUP_VISIBLE' => 'Y'
);
$arGroupByGR = false;
$arNavigationGR = false;
$arSelectFieldsGR = array(
'GROUP_ID',
'GROUP_NAME'
);
$rsItems = CSocNetUserToGroup::GetList($arOrderGR,
$arFilterGR, $arGroupByGR, $arNavigationGR,
$arSelectFieldsGR);
while($arItem = $rsItems->GetNext(false, false)) {
$id_ = intval($arItem['GROUP_ID']);
$arEntityGroupsID[$id_] = array(
'ID' => $id_,
'NAME' => $arItem['GROUP_NAME']
);
}
unset($rsItems, $id_, $arOrderGR, $arGroupByGR,
$arNavigationGR, $arSelectFieldsGR);
if(!empty($arEntityGroupsID)) {
//Определим группы, у которых блоги имеют статус
// приватных, и исключим их из списка
$arExceptGroupEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',
'view_post', array('!ROLE' => SONET_ROLES_ALL),
array('!ROLE' => SONET_ROLES_ALL));
$arTmp = array_intersect_key($arExceptGroupEntity,
$arEntityGroupsID);
unset($arExceptGroupEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityGroupsID[$key]);
}
}
unset($arTmp);
//Определим группы, в которых вообще отключены блоги,
// и исключим их из списка
$arExceptGroupEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',
'view_post', array('FEATURE_ACTIVE' => 'N'),
array('FEATURE_ACTIVE' => 'N'));
$arTmp = array_intersect_key($arExceptGroupEntity,
$arEntityGroupsID);
unset($arExceptGroupEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityGroupsID[$key]);
}
}
unset($arTmp);
}
}
Кроме того, необходимо обработать ситуацию, когда мы имеем дело с лентой для группы. Определяем, открыта ли группа для просмотра текущему пользователю, и вообще могут ли пользователи иметь блоги (листинг 6).
$getMembers = false;
if($arResult['CURRENT_ACCESS']['canViewGroup']) {
$getMembers = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_USER, 'blog');
}
// Определим ID участников группы
if($getMembers) {
$arOrderGM = array();
$arFilterGM = array(
'<=ROLE' => SONET_ROLES_USER,
'GROUP_ID' => $arResult['ENTITY_ID'],
);
$arGroupByGM = false;
$arNavigationGM = false;
$arSelectFieldsGM = array(
'USER_ID'
);
$rsItems = CSocNetUserToGroup::GetList($arOrderGM,
$arFilterGM, $arGroupByGM, $arNavigationGM,
$arSelectFieldsGM);
while($arItem = $rsItems->Fetch()) {
$id_ = intval($arItem['USER_ID']);
$arEntityUsersID[$id_] = $id_;
}
unset($rsItems, $id_, $arOrderGM, $arGroupByGM,
$arNavigationGM, $arSelectFieldsGM);
// код исключения "закрытых" пользователей вынесен в конец
// модуля (он общий для двух типов лент)
}
// Добавим ID группы в $arEntityGroupsID, если включен вывод
// сообщений из блога группы
if($arParams['INC_SELF_MESSAGES'] &&
$arResult['CURRENT_ACCESS']['canViewGroup'] &&
$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages']) {
//если блоги в группе не отключены
if(CSocNetFeatures::IsActiveFeature(SONET_ENTITY_GROUP,
$arResult['ENTITY_ID'], 'blog')) {
$arEntityGroupsID[$arResult['ENTITY_ID']] = array(
'ID' => $arResult['ENTITY_ID'],
'NAME' => $arResult['GROUP_INFO']['NAME']
);
}
}
Чуть выше мы отметили, что код исключения «закрытых» пользователей вынесен в конец компонента. Теперь пришло время определить пользователей, которые запретили просмотр блогов. Уберем их из списка (раз уж они сами этого хотят; листинг 7).
if(!empty($arEntityUsersID)) {
$arExceptUserEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',
'view_post', array('!ROLE' => SONET_RELATIONS_TYPE_ALL),
array('!ROLE' => SONET_RELATIONS_TYPE_ALL));
$arTmp = array_intersect_key($arExceptUserEntity,
$arEntityUsersID);
unset($arExceptUserEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key)
{
unset($arEntityUsersID[$key]);
}
}
unset($arTmp);
// Если активный пользователь – владелец ленты, повторно
// включим его ID, даже если он закрыл свой блог – ему
// заведомо можно просматривать свои записи
if($arParams['INC_SELF_MESSAGES'] && $arResult
['CURRENT_ACCESS']['canViewUserSelfMessages']) {
$arEntityUsersID[$arResult['ENTITY_ID']] =
$arResult['ENTITY_ID'];
}
// Определим пользователей, которые вообще отключили свои
// блоги, и исключим их из списка
$arExceptUserEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',
'view_post', array('FEATURE_ACTIVE' => 'N'),
array('FEATURE_ACTIVE' => 'N'));
$arTmp = array_intersect_key($arExceptUserEntity,
$arEntityUsersID);
unset($arExceptUserEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityUsersID[$key]);
}
}
unset($arTmp);
}
Здесь надо обратить внимание на две дополнительные функции, которые не входят в стандартный API ядра «1С-Битрикс: Управление сайтом»: CPTK_SocialNetwork::
GetByRoleFeaturesIdArray() и CPTK_SocialNetwork::IsAllowedFeature(). Они представляют собой часть библиотеки автора, их текст здесь не приводится (при желании библиотеку можно запросить у автора статьи). Первая, CPTK_SocialNetwork::GetByRoleFeaturesIdArray(), возвращает массив идентификаторов объектов по типу объекта, сигнатуре, функциональности и операциям (с возможностью установки дополнительного фильтра). Функция имеет внутреннее кэширование результатов, в ее основе лежит метод CSocNetFeaturesPerms::GetList(). Вторая, CPTK_SocialNetwork::IsAllowedFeature(), проверяет, доступна ли для заданного объекта затребованная возможность (это небольшая оптимизация часто выполняемой операции, опытному разработчику не составит труда реализовать ее самостоятельно).
На этом этапе возникает проблема: при организации группы социальной сети система не создает соответствующие записи в таблицах БД, где хранятся настройки прав доступа к заданным функциям. Эти записи автоматически создаются только после первого изменения прав доступа, до того они определяются системой, средствами PHP. Причем по умолчанию используется режим не «разрешено всем», а «разрешено только участникам». Следовательно, попытка выбрать все записи, у которых в поле ROLE не установлено значение константы SONET_RELATIONS_TYPE_ALL («разрешено всем»; в нашем случае это и будет вызов CPTK_SocialNetwork::GetByRoleFeaturesIdArray() с установленным дополнительным фильтром array('!ROLE' => SONET_RELATIONS_TYPE_ALL), приведет к получению неверного результата. Этот нюанс можно считать досадной ошибкой в архитектуре модуля, но, к счастью, дело поправимо без вмешательства в ядро системы. В нашем случае оказалось достаточно добавить обработчик события OnSocNetGroupAdd, где и выполняются необходимые для корректной записи в БД структуры прав операции. В группах, которые были созданы ранее, эта ошибка была исправлена с помощью «Мастера».
Двигаемся дальше – получаем массив блогов, из которых будут выбираться новые записи (листинг 8).
$arBlogID = array(); // массив блогов
if(!empty($arEntityUsersID)) { // по владельцу блога
$arOrderBlog = array();
$arFilterBlog = array(
'OWNER_ID' => array_keys($arEntityUsersID),
'GROUP_ID' => $arParams['BLOG_GROUP_ID'],
'GROUP_SITE_ID' => SITE_ID,
'ACTIVE' => 'Y'
);
unset($arEntityUsersID);
$arGroupByBlog = false; $arNavigationBlog = false;
$arSelectFieldsBlog = array('ID', 'SONET_GROUP_ID');
$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,
$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);
while($arItem = $rsItems->Fetch()) {
if(intval($arItem['SONET_GROUP_ID']) <= 0) {
$id_ = intval($arItem['ID']);
$arBlogID[$id_] = $id_;
}
}
unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,
$arNavigationBlog, $arSelectFieldsBlog);
}
if(!empty($arEntityGroupsID)) { // по группе блога в соцсети
$arOrderBlog = array();
$arFilterBlog = array(
'SOCNET_GROUP_ID' => array_keys($arEntityGroupsID),
'GROUP_ID' => $arParams['BLOG_GROUP_ID'],
'GROUP_SITE_ID' => SITE_ID,
'ACTIVE' => 'Y'
);
$arGroupByBlog = false; $arNavigationBlog = false;
$arSelectFieldsBlog = array('ID', 'OWNER_ID');
$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,
$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);
while($arItem = $rsItems->Fetch()) {
if(intval($arItem['OWNER_ID']) <= 0) {
$id_ = intval($arItem['ID']);
$arBlogID[$id_] = $id_;
}
}
unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,
$arNavigationBlog, $arSelectFieldsBlog);
}
Здесь все вполне прозрачно, комментарии не требуются. И наконец, выбираем записи из блогов, которые и станут основой для формирования конечного результата – «Ленты друзей» (листинг 9).
if(!empty($arBlogID)) {
$arFilter = array(
'BLOG_ACTIVE' => 'Y',
'BLOG_GROUP_SITE_ID' => SITE_ID,
'PUBLISH_STATUS' => BLOG_PUBLISH_STATUS_PUBLISH,
'BLOG_ID' => array_keys($arBlogID),
'ACTIVE' => 'Y'
);
unset($arBlogID);
if($arParams['MAX_DAYS_COUNT'] > 0) {
// задан промежуток времени для выборки сообщений 86400 —
// кэшируем на сутки
$from = intval(time() – $arParams['MAX_DAYS_COUNT']*86400);
$arFilter['>=DATE_PUBLISH'] = ConvertTimeStamp($from,
'FULL');
}
$arGroupBy = false;
$arSelectFields = array(
'ID',
'BLOG_ID',
'TITLE',
'DATE_PUBLISH',
'AUTHOR_ID',
'DETAIL_TEXT',
'BLOG_ACTIVE',
'BLOG_URL',
'BLOG_GROUP_ID',
'BLOG_GROUP_SITE_ID',
'AUTHOR_LOGIN',
'AUTHOR_NAME',
'AUTHOR_LAST_NAME',
'BLOG_USER_ALIAS',
'BLOG_OWNER_ID',
'BLOG_USER_AVATAR',
'NUM_COMMENTS',
'VIEWS',
'ATTACH_IMG',
'BLOG_SOCNET_GROUP_ID'
);
$rsItems = CBlogPost::GetList($arOrder, $arFilter,
$arGroupBy, $arNavParams, $arSelectFields);
$rsItems->bShowAll = $arParams['PAGER_SHOW_ALL'];
//создаем объект парсера сообщений блогов
$obParser = new blogTextParser(false,
$arParams['PATH_TO_SMILE']);
while($arItem = $rsItems->GetNext()) {
// здесь код разбора записи блога – ссылки, аватары,
// картинки, выполняем парсинг текста сообщения и т. д.
}
unset($obParser, $arOrder, $arGroupBy, $arSelectFields);
unset($arEntityGroupsID);
}
На этом содержательная часть работы заканчивается. Далее подключаем шаблон для оформления полученных данных HTML-кодом, и лента сообщений «как в ЖЖ» готова.
Легко заметить, что данный компонент не столь совершенен, каким мог бы быть. Скажем, напрашивается вопрос: а нельзя ли в «Ленте друзей» учитывать структуру связей пользователя с группами и другими пользователями социальной сети? Теоретически можно, на практике нагрузка на сервер возрастет в разы (если не на порядки), причем кэшировать что-либо будет невозможно. Причина – необходимость учитывать огромное количество комбинаций настроек, слишком много факторов будут определять итоговый результат.
Посмотрим, как будет влиять на содержимое «Ленты друзей» сложная конфигурация прав доступа. Предположим, что в профиле пользователя имеются настройки:
• «просматривать список друзей могут только друзья» – это означает, что прежде чем включать записи, опубликованные друзьями владельца ленты в блогах социальной сети, нужно выяснить, не является ли «текущий пользователь» («текущий пользователь» – пользователь, который в данный момент смотрит ленту) другом владельца ленты;
• «просматривать список друзей могут только друзья и друзья друзей» – прежде чем включать записи, опубликованные друзьями владельца ленты, нам нужно выяснить, не является ли текущий пользователь другом или другом друга владельца ленты;
• «просматривать список друзей могут все пользователи» – включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети;
• «полный запрет на просмотр друзей» – не включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети.
Настройки доступа к блогам пользователя:
• «просматривать сообщения могут все пользователи» – включаем записи блога в какую-либо ленту;
• «просматривать сообщения могут только друзья пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом владельца блога;
• «просматривать сообщения могут только друзья и друзья друзей пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом или другом друга владельца блога;
• «просматривать сообщения может только владелец блога» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь владельцем этого блога.
Настройки приватности группы:
• «группа видима всем посетителям» – включаем записи, опубликованные в блоге группы без проверки членства текущего пользователя в данной группе;
• «группа не видима всем посетителям» – прежде чем включить записи, опубликованные в блоге группы, нужно проверить, является ли текущий пользователь ее членом.
Настройки доступа к блогам группы:
• «просматривать сообщения блога могут только члены группы» – это означает, что, прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее членом;
• «просматривать сообщения блога могут только владелец группы и модераторы» – прежде чем включать записи, опубликованные в блоге группы, нужно выяснить, является ли пользователь ее владельцем или модератором;
• «просматривать сообщения блога могут все пользователи» – включаем записи блога группы без дополнительной проверки прав;
• «просматривать сообщения блога может только владелец группы» – прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее владельцем.
Все это придется проверять для каждого (!) блога, который будет попадать в чью-либо ленту. Наглядный пример. Допустим, Иван состоит в группе «Любители виски», которая видима всем посетителям сайта, но сообщения блогов могут читать только члены группы, и свой блог Иван разрешает читать только своим друзьям. Петя состоит в группе «Любители молока», которая видима всем посетителям сайта, и сообщения блогов открыты для всех. Петя – друг Ивана и читать сообщения из своего блога тоже разрешает только друзьям. Маша не состоит в указанных группах и сообщения из своего блога разрешает читать всем посетителям сайта. При этом Маша – друг Пети.
Теперь, если Петя захочет почитать ленту Ивана, то ему должны быть доступны только сообщения из блога Ивана. Если же Иван будет читать ленту Пети, то он должен видеть сообщения из блога Пети и из блога группы «Любителей молока». Маша, посетив ленту Ивана, вообще не должна видеть сообщений, а в ленте Пети – видеть только сообщения из группы «Любителей молока». Если Иван или Маша посетят ленту группы «Любителей молока», то они должны видеть сообщения из блога группы и сообщения из блога Пети. В ленте Маши, Вася и Петя должны будут видеть только сообщения из блога Маши.
Таким образом, для каждого посетителя каждой ленты придется генерировать уникальный кэш, что совершенно противопоказано для метода «полного кэширования результата» (когда сохраняется полностью готовый результат и на время жизни кэша он выдается без единого запроса к базе данных и вычислений в рамках логики компонента). Если предположить, что каждая лента будет состоять, скажем, из 10 страниц, а всего активных участников социальной сети (без учета групп!), например, 1000, то только для лент пользователей будет генерироваться 10 страниц ленты × 1000 лент × 1000 пользователей = 10 000 000 кэш-файлов. Если каждый кэш-файл будет занимать порядка 30 000 байт дискового пространства, то суммарный объем кэш-файлов только лент будет составлять 10 000 000 × 30 000 = 300 000 000 000 байт (≈279 Гбайт)! Мягко говоря, немало.
Как вариант, можно было бы использовать метод генерации страниц ленты с частичным, но не значимым кэшированием. Но полумеры не спасают. Нагрузка на сервер возрастает, несущественно снижая объем кэша (в экспериментах – примерно в три раза) и радикально усложняя логику обработки (как минимум придется тщательно заботиться о сбросе некорректного кэша), отладку и сопровождение модуля.
В итоге при создании социальной сети для сайта www.pcmag.ru было принято решение реализовать ленту примерно в том виде, как описано в данной статье (ряд мелких деталей опущен для удобочитаемости). Да и, как показывает практика, особенной надобности в подключении к «Ленте друзей» дополнительных сущностей в общем-то не возникает. При необходимости (например, если все пользователи вдруг дружно возжелали видеть фотографии друг друга) компонент может быть доработан и расширен.
Drupaname = "note" разработка модуля
Роман Архаров
Данная статья – продолжение материала, посвященного CMS Drupal (см. PC Magazine/RE, 12/2008). В первой статье подробно рассказано о назначении и возможностях системы, а также приведены примеры сборки сайтов на Drupal с использованием уже существующих модулей. Этот же материал будет больше интересен техническим специалистам, умеющим программировать на языке PHP, знакомым с основами HTML и CSS, и тем, кто хочет больше узнать о методах разработки собственных модулей для этой системы. Предыдущая статья доступна сейчас в Интернете по адресу: www.pcmag.ru/solutions/detail.php?ID=32535. Перед чтением этого материала рекомендуется освежить в памяти информацию, просмотрев ее первые три раздела.
Система управления сайтом Drupal построена по модульному принципу: компактный набор служебных функций (ядро) расширяется при помощи модулей – файлов с PHP-кодом. Модули должны содержать «хуки» (hooks) – особым образом именованные функции, которые вызываются ядром Drupal при возникновении каких-либо событий. Каждый модуль имеет системное имя, которое должно состоять из латинских букв, цифр, знака подчеркивания (и начинаться обязательно с буквы). Имя хука должно состоять из двух частей: имени модуля и названия события. При возникновении любого события ядро Drupal в каждом из установленных модулей ищет и выполняет соответствующую функцию, т. е. функцию с именем название_модуля_название_события. Например, при возникновении событий, связанных с учетной записью пользователя (регистрация, авторизация, изменение роли пользователя и др.), ядро Drupal вызывает функции, реализующие хук hook_user, поэтому, чтобы модуль с именем example мог отреагировать на это событие, в нем необходимо объявить функцию с именем example_user(). Список передаваемых в эту функцию аргументов, пример ее использования и информацию обо всех функциях и хуках, доступных в Drupal, можно найти на странице официальной документации http://api.drupal.org или ее русской версии: http://api.drupal.ru.
Каждый модуль для Drupal представляет два файла или более, которые должны находиться в папке sites/all/modules/название_модуля[1].
В файле название_модуля.info должна находиться служебная информация, а в файле название_модуля.module – исходный текст. При наличии этих двух файлов модуль станет доступным на странице установки модулей Drupal (Administer – Modules, admin/build/modules). Кроме того, в этой же папке может находиться необязательный файл название_модуля.install, содержащий реализации хуков, которые будут выполнены при инсталляции модуля. В этом файле обычно располагаются инструкции, создающие новые таблицы в базе данных и задающие значения по умолчанию для настроек модуля.
Для иллюстрации использования системы хуков я приведу пример разработки простого модуля, который формирует блок с информацией о курсах валют. Этот модуль при выполнении соответствующей строки cron-таблицы будет соединяться с сервером ЦБ РФ и получать от него информацию о курсах валют. На основе полученных данных и настроек, заданных через интерфейс управления модулем, а также функций темизации будет генерироваться выходный HTML-код. Он будет кэшироваться стандартными средствами Drupal (благодаря чему администратор при желании сможет перенести этот кэш из базы данных, например, в файловую систему или memcache) и выдаваться по запросу пользователя в виде блока.
Модуль получит название currencies. На первом этапе его разработки необходимо (относительно корня Drupal-сайта) создать папку sites/all/modules/currencies, в которой мы будем сохранять новые файлы.
currencies.info
В .info-файлах модулей содержится служебная информация, без которой модуль не будет виден в системе. Начинаться любой .info-файл должен со строки
; $Id$
В файлах с PHP-кодом после открывающего тега <?php необходимо добавить строку
// $Id$
Эту строку, если модуль будет размещен в официальном CVS-репозитории Drupal, заменит служебная информация.
Далее в файле .info должны располагаться три обязательных параметра: название модуля, его описание и версия ядра Drupal, с которой работает модуль. Кроме того, в этом файле могут находиться необязательные параметры: минимальная версия PHP, необходимая для запуска модуля, зависимость от других модулей Drupal, без которых текущий модуль не будет работать, и пр. Подробное описание всех доступных к использованию в .info-файле параметров можно найти в официальной документации (ссылка на эту и другие цитируемые в статье страницы документации размещена во врезке «Ссылки на документацию»).
В нашем случае файл currencies.info будет иметь такой вид:
; $Id$
name = Currencies block
description = Show currencies
core = 6.x
Хотя один файл в нашем модуле уже есть, но пока он отсутствует в списке имеющихся в системе модулей, и мы переходим к следующему файлу.
currencies.install
По стандартам кодирования Drupal перед каждой функцией, реализующей хук, должен быть размещен комментарий вида:
/*
* Implementation of hook_название_хука().
*/
Аналогичный комментарий (только без текста Implementation of...) с описанием функции и ее аргументов должен располагаться перед любой другой функцией. Эти комментарии используются системой генерации документации Doxygen, по ним также удобно вести поиск. Здесь для экономии места такие комментарии опущены.
Система управления сайтом Drupal построена по модульному принципу: ядро расширяется при помощи модулей – файлов с PHP-кодом.
При инсталляции и деинсталляции модулей вызываются хуки hook_install и hook_uninstall. Отмечу, что в Drupal кроме понятий инсталляции и деинсталляции есть понятия активации и деактивации модуля. Если модуль устанавливается впервые (в административном интерфейсе, в списке модулей установлена галочка напротив нужного модуля и нажата кнопка Submit), сначала происходит событие install, затем событие enable, т. е. ядро Drupal ищет и, если находит, вызывает функции, реализующие хуки hook_install и hook_enable для устанавливаемого модуля. Далее, если администратор выключает модуль, то происходит событие disable и вызывается функция, реализующая хук hook_disable. В следующий раз, когда модуль будет вновь включен, произойдет только событие enable, а не install. Если модуль был сначала деактивирован, а затем удален (удаление производится на отдельной от списка модулей странице), то происходят события disable и uninstall и в следующий раз при включении модуля опять произойдут события install и enable.
Такое разделение очень удобно. Обычно при возникновении события install программисты создают необходимые для работы модуля таблицы в базе данных, а при событии uninstall – удаляют их, таким образом после деинсталляции модуля в системе не остается никаких свидетельств его присутствия. При включении и выключении модуля (enable и disable) никакие сохраненные модулем данные не удаляются, а лишь отключается функционал модуля.
При возникновении события install программисты создают необходимые для работы модуля таблицы в базе данных
Вернемся к нашему примеру. Информацию о курсах валют разрабатываемый модуль currencies будет получать с сервера Центрального Банка РФ. Чтобы при каждом показе блока не обращаться с запросом к удаленному серверу, данные будут сохраняться в базе данных нашего сайта. В самом простом случае для хранения данных можно было бы воспользоваться функциями cache_set или variable_set из ядра Drupal, однако такой подход не очень удобен, когда нужно хранить информацию о курсах валют за длительный период, например, для последующего ее анализа. Поэтому мы создадим отдельную таблицу в базе данных и в ней будем хранить всю полученную информацию.
Наш пример довольно прост, поэтому события enable и disable использоваться в нем не будут, а на install и uninstall мы назначим функции создания и удаления таблицы в БД, для чего в файле currencies.install разместим функции, реализующие хуки hook_install и hook_uninstalname = "note"
function currencies_install() {
drupal_install_schema('currencies');
}
function currencies_uninstall() {
drupal_uninstall_schema('currencies');
}
Функции drupal_install_schema и drupal_uninstall_schema являются частью Drupal Schema API. Schema API – это слой абстракции от базы данных, благодаря которому программист может не задумываться о том, как адаптировать свой SQL-запрос под ту или иную базу данных, ему достаточно сформировать массив определенного вида и передать его одной из функций Schema API, после чего этот массив будет преобразован в корректный SQL-запрос к той базе данных, которая используется с Dupal. Единственный аргумент, который принимают эти две функции, – название модуля, схема которого будет установлена или удалена, т. е. после вызова drupal_install_schema('currencies') Drupal попытается создать таблицы, описанные в реализации hook_schema модуля currencies, поэтому нужно создать эту реализацию (см. листинг 1). Этот хук должен возвращать ассоциативный массив, содержащий информацию о создаваемых таблицах. В нашем примере создается таблица с именем currencies_block и двумя полями: timestamp и data. Подробное описание формата возвращаемого массива можно найти в документации.
function currencies_schema() {
$schema['currencies_block'] = array(
'description' => t('Some table description.'),
'fields' => array(
'timestamp' => array(
'type' => 'int',
'size' => 'normal',
'not null' => TRUE,
'default' => 0,
),
'data' => array(
'type' => 'text',
'not null' => TRUE,
),
),
);
return $schema;
}
На данном этапе в нашем модуле currencies есть два файла: currencies.info и currencies.install, но он по-прежнему недоступен для выбора на странице со списком модулей, поскольку в нем отсутствует самый важный файл – .module. Если в папке модуля создать файл currencies.module и разместить в нем всего две строчки:
<?php
// ; $Id$
(их описание было дано выше), модуль тут же станет доступным для установки, однако, так как файл currencies.module не содержит никаких инструкций, установка этого модуля приведет только к созданию одной таблицы в БД и он не будет нам полезен. Поэтому мы переходим к самому большому, сложному и важному этапу – разработке основного функционала модуля.
currencies.module
В Drupal реализован механизм «ролей» и «прав доступа». По умолчанию в системе есть две «роли» (группы) пользователей – анонимы и авторизованные пользователи, одна из которых автоматически присваивается каждому пользователю в зависимости от того, авторизован он в системе или нет. Каждый модуль может объявить «права доступа» к своим ресурсам, после чего администратор сайта через специальный интерфейс может установить, какие роли имеют доступ к каким сервисам сайта. Например, обычно пользователи, входящие в группу авторизованных, могут вести персональный блог, а у анонимов такой возможности нет. В любой момент администратор сайта через Web-интерфейс может создать дополнительные роли (модераторы, администраторы и т. п.), определить особые разрешения для каждой из ролей (например, модераторы могут снимать с публикации материалы и комментарии, а администраторы их безвозвратно удалять) и назначать эти роли любому пользователю, причем ему одновременно может быть присвоено несколько ролей.
В интерфейсе администратора с помощью hook_menu и Forms API будет создана форма, позволяющая указать список валют.
Выше уже была описана логика работы разрабатываемого модуля, сейчас, перед тем как написать первые строки кода, необходимо более подробно продумать алгоритм работы программы. Если говорить о нашем случае, то программа будет разбита на четыре основные части: