-
Notifications
You must be signed in to change notification settings - Fork 0
События: модуль и обработчики
Для программиста термин "событие" сразу подразумевает что-то асинхронное. Но нет, в UMI CMS это просто методы и просто модуль в админке. И этот модуль вообще говоря с методами никак не связан. Но обо всем по порядку.
Скажем, на своем сайте мы используем модуль "Блоги". Если пользователь написал очередной пост, мы бы хотели отправить письмо модератору сайта, чтобы он мог разрешить публикацию поста. Если пользователь написал комментарий к чужому посту, мы бы хотели отправить письмо автору поста, и, возможно, опять же модератору. Если модератор написал комментарий через админку, тоже неплохо бы отправить письмо автору поста. И в каждом из этих случаев мы хотим увидеть запись о событии в модуле "События" админки.
Часть нужной функциональности уже есть прямо "из коробки". В каждом модуле присутствуют следующие файлы:
- events.php - содержит список событий, которые надо "отслеживать", и их обработчиков
- handlers.php - содержит обработчики событий
Если посмотреть на содержимое этих файлов в модуле blogs20, то увидим список уже имеющихся обработчиков:
- onCommentAdd - иетод, отправляющий письмо автору поста блога о новом комментарии
- onPostAdded и onCommentAdded - методы, проверяющие на наличие спама
В какой момент вызывается обработчик? Смотрим код метода commentAdd() в файле macros.php модуля blogs20, и видим:
...
$oEventPoint = new umiEventPoint('blogs20CommentAdded');
$oEventPoint->setMode('after');
$oEventPoint->setParam('id', $iCommentId);
$oEventPoint->setParam('template', $template);
blogs20::setEventPoint($oEventPoint);
...
Короче, создаем объект umiEventPoint в методе обработки запроса там, где удобно. Не буду останавливаться на свойствах события "before" и "after", для наших задач все будут "after". Кстати, параметров можно передать сколько и каких угодно, всё, что может нам понадобиться в обработчике события.
Будет ли событие blogs20CommentAdded добавления комментария отражено в модуле События? Да, потому что оно есть в списке отслеживаемых в модуле events. Открываем файл events.php модуля events, и видим в самом начале списка:
<?php
use UmiCms\Service;
if (!Service::Registry()->get('//modules/events/collect-events')) {
Service::EventHandlerFactory()->createForModuleByConfig([
[
'event' => 'blogs20PostAdded',
'method' => 'onBlogsPostAdded'
],
[
'event' => 'blogs20CommentAdded',
'method' => 'onBlogsCommentAdded'
],
[
'event' => 'comments_message_post_do',
'method' => 'onCommentsCommentPost'
],
...
Перечисленные в списке методы описаны в файле handlers.php модуля events. Очевидно, обработчики событий модулей blogs20 и events никак не связаны, и если мы добавим кастомное событие в модуль blogs20, фиксироваться в модуле events оно не будет.
Для счастья заказчика нам не хватает:
- Отправки сообщения модератору при создании поста блога на клиентской части
- Отправки сообщения автору поста блога, если модератор в админке написал комментарий
И все эти события должны отображаться в модуле События.
Нам повезло, это событие уже есть, и оно есть даже в списке модуля events, поэтому всё, что требуется - это заменить код обработчика, добавив туда кусок для отправки сообщения модератору. Поэтому копируем код метода onPostAdded в файл customMacros.php модуля blogs20 и добавляем туда фрагмент отправки сообщения (можно скопировать код целиком из обработчика onCommentAdd, описанного в файле handlers.php модуля blogs20, а затем слегка модифицировать):
/**
* Обработчик события создания поста с клиентской части
* @param iUmiEventPoint $event событие создания поста
*/
public function onPostAdded(iUmiEventPoint $event) {
if ($event->getMode() != 'after') return false;
$umiHierarchy = umiHierarchy::getInstance();
$umiObjects = umiObjectsCollection::getInstance();
$umiHierarchyTypes = umiHierarchyTypesCollection::getInstance();
$postId = $event->getParam('id');
antiSpamHelper::checkForSpam($postId);
$templateParam = $event->getParam('template');
$template = $templateParam ?: 'default';
$post = $umiHierarchy->getElement($postId, true);
$parentId = $post->getParentId();
$element = $umiHierarchy->getElement($parentId);
$blogHierarchyTypeId = $umiHierarchyTypes->getTypeByName('blogs20', 'blog')->getId();
$blog = $element;
if (!$blog instanceof iUmiHierarchyElement) {
return false;
}
while ($blog->getTypeId() != $blogHierarchyTypeId) {
$blog = $umiHierarchy->getElement($blog->getParentId(), true);
}
$parentOwner = $umiObjects->getObject($element->getObject()->getOwnerId());
if (!$parentOwner instanceof iUmiObject) {
return false;
}
$subjectTemplateLabel = 'post_for_blog_subj';
$contentTemplateLabel = 'post_for_blog_body';
$notificationName = 'notification-blogs-post-add-admin';
$subjectTemplateName = 'blogs-post-add-notification-subject-admin';
$contentTemplateName = 'blogs-post-add-notification-content-admin';
$domain = Service::DomainDetector()->detectUrl();
$link = $domain . '/admin/blogs20/edit/' . $postId;
$variables = [
'link' => $link,
'title' => $post->title,
'content' => $post->content,
];
$objectList = [$post->getObject(), $parentOwner];
$subject = null;
$content = null;
if ($this->module->isUsingUmiNotifications()) {
$mailNotifications = Service::MailNotifications();
$notification = $mailNotifications->getCurrentByName($notificationName);
if ($notification instanceof MailNotification) {
$subjectTemplate = $notification->getTemplateByName($subjectTemplateName);
$contentTemplate = $notification->getTemplateByName($contentTemplateName);
if ($subjectTemplate instanceof MailTemplate) {
$subject = $subjectTemplate->parse($variables, $objectList);
}
if ($contentTemplate instanceof MailTemplate) {
$content = $contentTemplate->parse($variables, $objectList);
}
}
} else {
try {
list($subjectTemplate, $contentTemplate) = blogs20::loadTemplatesForMail(
'blogs20/mail/' . $template,
$subjectTemplateLabel,
$contentTemplateLabel
);
$subject = blogs20::parseTemplateForMail($subjectTemplate, $variables);
$content = blogs20::parseTemplateForMail($contentTemplate, $variables);
} catch (Exception $e) {
// nothing
}
}
if ($subject === null || $content === null) {
return false;
}
$mailSettings = $this->module->getMailSettings();
$emailTo = $mailSettings->getAdminEmail();
$fromEmail = $mailSettings->getSenderEmail();
$fromName = $mailSettings->getSenderName();
$mail = new umiMail();
$mail->addRecipient($emailTo, $emailTo);
$mail->setFrom($fromEmail, $fromName);
$mail->setSubject($subject);
$mail->setContent($content);
$mail->commit();
$mail->send();
return true;
}
Почти всё готово, за исключением шаблона письма, который мы указали в $notificationName и других переменных, относящихся к шаблону уведомления. К сожалению, в модуле "Шаблоны уведомлений" нужного нам шаблона нет вообще. Надо его создать. Как это сделать, хорошо описано в разделе Системные уведомления документации. Для наших задач скрипт создания нового уведомления будет примерно таким:
<?php
include 'standalone.php';
$mailNotifications = UmiCms\Service::MailNotifications();
$mailNotificationsMap = $mailNotifications->getMap();
$mailTemplates = UmiCms\Service::MailTemplates();
$mailTemplatesMap = $mailTemplates->getMap();
$defaultLangId = 1;
$defaultDomainId = 1;
$newNotification = $mailNotifications->create([
$mailNotificationsMap->get('LANG_ID_FIELD_NAME') => $defaultLangId,
$mailNotificationsMap->get('DOMAIN_ID_FIELD_NAME') => $defaultDomainId,
$mailNotificationsMap->get('NAME_FIELD_NAME') => 'notification-blogs-post-add-admin',
$mailNotificationsMap->get('MODULE_FIELD_NAME') => 'blogs20',
]);
$subjectTemplate = $mailTemplates->create([
$mailTemplatesMap->get('NOTIFICATION_ID_FIELD_NAME') => $newNotification->getId(),
$mailTemplatesMap->get('NAME_FIELD_NAME') => 'notification-blogs-post-add-subject-admin',
$mailTemplatesMap->get('TYPE_FIELD_NAME') => 'post_for_blog_subj',
$mailTemplatesMap->get('CONTENT_FIELD_NAME') => '%header%',
]);
$contentBody = <<<CONTENT
<p></p>
CONTENT;
$contentTemplate = $mailTemplates->create([
$mailTemplatesMap->get('NOTIFICATION_ID_FIELD_NAME') => $newNotification->getId(),
$mailTemplatesMap->get('NAME_FIELD_NAME') => 'notification-blogs-post-add-content-admin',
$mailTemplatesMap->get('TYPE_FIELD_NAME') => 'post_for_blog_body',
$mailTemplatesMap->get('CONTENT_FIELD_NAME') => $contentBody,
]);
echo "ok";
?>
После однократного запуска скрипта шаблон уведомления станет доступен в модуле "Шаблоны уведомлений". Чтобы вместо внутренних названий полей в шаблонах отображались нормальные имена, надо добавить их в файл /classes/modules/data/18n.php нашего шаблона:
<?php
/** Языковые константы для русской версии */
$i18n = [
...
'notification-blogs-post-add-admin' => 'Новое сообщение в блог',
'mail-template-notification-blogs-post-add-subject-admin' => 'Тема письма',
'mail-template-notification-blogs-post-add-content-admin' => 'Шаблон письма',
]
Новый шаблон можем оформить согласно пожеланиям заказчика, используя в тексте шаблона передаваемые в обработчике события переменные $variables в формате %имя_переменной%.
После всех манипуляций получаем то, что и хотел заказчик: при создании нового поста блога это событие фиксируется в модуле События (потому что это событие есть в списке модуля События), и письмо об этом отправляется модератору (адрес которого указан в разделе Конфигурвция в админке).
Только что мы совершили непростительный поступок - переопределили стандартный метод onPostAdded. Зато быстро. А теперь надо сделать всё правильно, и создать свой обработчик, назовем его onPostAdd, а onPostAdded пусть как и раньше на спам проверяет.
В customMacros.php меняем имя метода на onPostAdd, и письма перестают отправляться. Чтобы восстановить нужную функциональность, надо:
- Скопировать из macros.php в customMacros.php метод postAdd() и изменить в вызове new umiEventPoint('blogs20PostAdded') название события blogs20PostAdded на, скажем, blogs20PostAdd
- Если делать все правильно, надо переименовать метол postAdd() в customMacros.php и соответствующий ему шаблон в templates/, пусть будет newPost()
- Создать файл permissions.custom.php и добавить в него код:
<?php
$permissions['add'][] = 'newpost';
- Создать файл custom_events.php и добавить туда код:
<?php
use UmiCms\Service;
if (!Service::Registry()->get('//modules/events/collect-events')) {
new umiEventListener("blogs20PostAdd", "blogs20", "onPostAdd");
}
- Поскольку событие blogs20PostAdd отсутствует в списке событий модуля events, то оно не будет отображаться в этом модуле. Добавление события в custom_events.php модуля events не приводит к регистрации события! Правда, теперь письмо приходит 2 раза, т е наш обработчик запускается дважды, один раз в модуле blogs20, второй раз в модуле events. Чтобы исправить ситуацию, посмотрим, как же обрабатываются события в этом модуле. Все обработчики имеют почти одинаковый код:
...
/**
* Обработчик события создания поста модуля "Блоги"
* Создает соответствующее событие
* @param iUmiEventPoint $event событие создания поста блога.
*/
public function onBlogsPostAdded(iUmiEventPoint $event) {
if ($event->getMode() == 'after') {
$postId = $event->getParam('id');
$post = umiHierarchy::getInstance()->getElement($postId, true, true);
/** @var blogs20 $module */
$module = cmsController::getInstance()->getModule('blogs20');
$links = $module->getEditLink($postId, 'post');
if (isset($links[1])) {
$this->module->registerEvent('blogs20-post-add', [$links[1], $post->getName()], $postId);
}
}
}
...
и регистрация события происходит при вызове $this->module->registerEvent(). У метода 4 параметра, подробнее можно прочесть в разделе Кастомные события для модуля "События". Для нас важны первые 2 параметра.
Первый - это строковый идентификатор события, в документации сказано, что он может быть любым, но это не так. Он обязательно должен начинаться с названия модуля, остальная часть через дефис. И этот идентификатор надо прописать в файле i18n.php. Но файл этот может быть затерт при обновлении системы, поэтому лучше скопировать его в файл i18n.ru.php и добавлять параметры уже туда:
...
'blogs20-post-add' => 'Новое сообщение в блог',
'blogs20-post-add_msg' => 'Пользователь <a href="%s">%s</a> написал сообщение "%s"',
'blogs20-post-add_new' => 'Новое сообщение в блог',
'blogs20-post-add_img' => '/images/cms/admin/mac/icons/medium/post.png'
Понятно, что вместо %s подставляются элементы переданного во втором параметре массива, причем первые 2 - ссылка на редактирование пользователя и его логин - передаются автоматически, их не надо добавлять в массив. И 'blogs20-post-add_img' вообще никак не используется, кстати. Адрес пиктограммы модуля формируется из его названия - того, что перед дефисом.
Не слишком просто. Зато теперь мы можем добавить в свой обработчик onPostAdd в самый конец вместо return true;
...
$eventsModule = cmsController::getInstance()->getModule('events');
$eventsModule->registerEvent('blogs20-post-add', [$postId , $post->getName()], null, null);
custom_events.php в модуле events нам больше не нужен, удаляем. Вообще хорошо, что все кастомные методы и события теперь находятся в модуле blogs20, а в модуле events содержится только i18n.ru.php с новым идентификатором события.
Необходимо добавить, что если мы создаем свой собственный модуль, то все события можем спокойно складывать в файл events.php, а обработчики в файл handlers.php своего модуля.
Осталась последняя задача: когда модератор комментирует пользовательское сообщение через админку, отправлять пользователю письмо и фиксировать событие в модуле События.
Не стоит добавлять код в административные модули, воспользуемся системными событиями, их список можно посмотреть на странице Стандартные точки вызова документации. Очевидно, нам подходит systemCreateElement. Поэтому добавляем в файл custom_events.php модуля blogs20 одну строку:
<?php
use UmiCms\Service;
if (!Service::Registry()->get('//modules/events/collect-events')) {
new umiEventListener("blogs20PostAdd", "blogs20", "onPostAdd");
new umiEventListener("systemCreateElement", "blogs20", "onAdminCommentAdd");
}
Для работы административной части используются кастомные методы из файла customAdmin.php, поэтому добавляем наш обработчик именно туда:
<?php
use UmiCms\Service;
/** Класс пользовательских методов административной панели */
class EventsCustomAdmin {
/** @var events $module */
public $module;
/**
* Обработчик события создания комментария в административной части.
* Отправляет уведомление о комментарии автору поста.
* @param iUmiEventPoint $eventPoint событие создания комментария
* @return bool
* @throws coreException
* @throws publicException
* @throws Exception
*/
public function onAdminCommentAdd(iUmiEventPoint $event) {
if($event->getMode() != 'after') {
return;
}
$element = $event->getRef('element');
if(!$element instanceof umiHierarchyElement) {
return;
}
$umiObjects = umiObjectsCollection::getInstance();
$typesCollection = umiObjectTypesCollection::getInstance();
$commentTypeId = $typesCollection->getBaseType('blogs20', 'comment');
$elementTypeId = $element->getObject()->getTypeId();
if($elementTypeId != $commentTypeId) {
return;
}
$pageId = $event->getRef('element')->getId();
$hierarchy = umiHierarchy::getInstance();
$page = $hierarchy->getElement($pageId);
$authorId = $element->getObject()->getOwnerId();
$author = $umiObjects->getObject($authorId);
$authorName = $author->getValue('lname') . ' ' . $author->getValue('fname') . ' ' . $author->getValue('father_name');
$parentId = $hierarchy->getParent($pageId);
if ($parentId) {
$parent = $hierarchy->getElement($parentId);
$postTypeId = $typesCollection->getBaseType('blogs20', 'post');
$post = $parent;
if (!$post instanceof iUmiHierarchyElement) {
return false;
}
while ($post->getObject()->getTypeId() != $postTypeId) {
$post = $hierarchy->getElement($post->getParentId(), true);
}
if ($parent->getObject()->getTypeId() == $postTypeId) {
$subjectTemplateLabel = 'comment_for_post_subj';
$contentTemplateLabel = 'comment_for_post_body';
$notificationName = 'notification-blogs-post-comment';
$subjectTemplateName = 'blogs-post-comment-subject';
$contentTemplateName = 'blogs-post-comment-content';
} else {
$subjectTemplateLabel = 'comment_for_comment_subj';
$contentTemplateLabel = 'comment_for_comment_body';
$notificationName = 'notification-blogs-comment-comment';
$subjectTemplateName = 'blogs-comment-comment-subject';
$contentTemplateName = 'blogs-comment-comment-content';
}
$publishTime = $post->publish_time;
$post_time = date("d.m.Y", ($publishTime instanceof umiDate)? $publishTime->getFormattedDate('U') : (int) $publishTime );
$parentOwner = $umiObjects->getObject($post->getObject()->getOwnerId());
if ($parentOwner instanceof iUmiObject) {
$email = $parentOwner->getValue('e-mail');
$nick = $parentOwner->getValue('login');
$firstName = (string) $parentOwner->getValue('fname');
$lastName = $parentOwner->getValue('lname');
$fatherName = $parentOwner->getValue('father_name');
$name = $firstName !== '' ? ($firstName . ' ' . $fatherName . ' ' . $lastName) : $nick;
if (trim($name) == '') {
$name = $parentOwner->getValue('login');
}
if (trim($email) == '') {
$email = $parentOwner->getValue('login');
}
$domain = Service::DomainDetector()->detectUrl();
$link = $domain . '/blogs20/postView/' . $post->getId() . '/#comment_' . $commentId;
$pageContent = $page->content;
$variables = [
'name' => $name,
'author' => $authorName,
'content' => $pageContent,
'post_time' => $post_time,
'link' => $link,
];
$objectList = [$page->getObject(), $parentOwner, $author];
$subject = null;
$content = null;
if ($this->module->isUsingUmiNotifications()) {
$mailNotifications = Service::MailNotifications();
$notification = $mailNotifications->getCurrentByName($notificationName);
if ($notification instanceof MailNotification) {
$subjectTemplate = $notification->getTemplateByName($subjectTemplateName);
$contentTemplate = $notification->getTemplateByName($contentTemplateName);
if ($subjectTemplate instanceof MailTemplate) {
$subject = $subjectTemplate->parse($variables, $objectList);
}
if ($contentTemplate instanceof MailTemplate) {
$content = $contentTemplate->parse($variables, $objectList);
}
}
} else {
try {
list($subjectTemplate, $contentTemplate) = blogs20::loadTemplatesForMail(
'blogs20/mail/' . $template,
$subjectTemplateLabel,
$contentTemplateLabel
);
$subject = blogs20::parseTemplateForMail($subjectTemplate, $variables);
$content = blogs20::parseTemplateForMail($contentTemplate, $variables);
} catch (Exception $e) {
// nothing
}
}
if ($subject != null || $content != null) {
$mailSettings = $this->module->getMailSettings();
$fromEmail = $mailSettings->getSenderEmail();
$fromName = $mailSettings->getSenderName();
$mail = new umiMail();
$mail->addRecipient($email, $name);
$mail->addRecipient('[email protected]', $name);
$mail->setFrom($fromEmail, $fromName);
$mail->setSubject($subject);
$mail->setContent($content);
$mail->commit();
$mail->send();
}
}
}
if ($parentId) $postName = $post->getName();
else $postName = $page->getName();
$eventsModule = cmsController::getInstance()->getModule('events');
$eventsModule->registerEvent('blogs20-systemAddCommentElement', [$pageId, $postName], null, null);
}
}
Чтобы не создавать нового шаблона уведомления, использовали уже существующие "Комментарий к посту" и "Комментарий к комментарию". А вот строковые константы в файл i18n.ru.php модуля events придется добавить:
...
'blogs20-systemAddCommentElement' => 'Новый комментарий',
'blogs20-systemAddCommentElement_msg' => 'Модератор <a href="%s">%s</a> оставил комментарий "%s"',
'blogs20-systemAddCommentElement_new' => 'Новый комментарий',
'blogs20-systemAddCommentElement_img' => '/images/cms/admin/mac/icons/medium/comment.png'
Осталось добавить, что если мы создаем свой собственный модуль, то все события можем спокойно складывать в файл events.php, а обработчики в файл handlers.php своего модуля. Как для пользовательской, так и для административной части.
В результате длительной переписки с техподдержкой я так и не могу ответить на этот вопрос. В документации содержатся противоречивые рекомендации. В частности, советуют создавать кастомный обработчик непосредственно в модуле events (см. Добавление события об изменении страницы или смены активности страницы).
Да, так тоже работает. Приведенный же выше метод хорош тем, что кастомные файлы находятся только в своем модуле, а в модуль events поместили всего один дополнительный файл с константами для красивого показа кастомных событий в админке.
Короче, замечания и исправления всячески приветсятвуются!