Хуки-хухуки: Добавляем ссылки под заголовки виджетов

+17
1.39K
Вторая версия InstantCMS понравилась мне ещё с релиз-кандидатов — очень продуманная система. Но ещё с тех давних времён я хотел добавить ссылки под заголовками виджетов. Предложенный вариант — не самый простой. Зато очень хорошо подходит для изучения работы с хуками.


Подготовка

Мы будем использовать систему событий и хуков Двойки. Поэтому для начала нужно понять Что такое события и хуки) и сделать себе Компонент для хуков. Если у вас уже есть свой готовый компонент, то можно использовать его.

Вся задача состоит из трёх подзадач:
1. Найти событие, в котором можно будет добавить поле со ссылкой в опции виджета.
2. Найти событие или другой способ, чтобы вывести ссылку из предыдущего пункта под заголовок виджета.
3. Добавить опцию в наш компонент, чтобы можно было включать или отключать эту возможность для разных проектов. На мой взгляд, это правильная практика — запускать все хуки компонента через их опции.

Опция в настройках

Начнём с простого и уже известного – добавим опцию. У нас уже есть тестовый чекбокс 'is_it_works' в наборе полей 'Тест'. Так что мы просто переименуем этот чекбокс на 'widgets_title_link' и языковые константы поменяем на нужные.
\system\controllers\webman\backend\forms\form_options.php
  1. <?php
  2.  
  3. class formWebmanOptions extends cmsForm {
  4.  
  5. public $is_tabbed = true;
  6.  
  7. public function init() {
  8.  
  9. return array(
  10.  
  11. 'type' => 'fieldset',
  12. 'title' => LANG_WEBMAN_OPT_TAB_WIDGETS,
  13. 'childs' => array(
  14.  
  15. new fieldCheckbox('widgets_title_link', array(
  16. 'title' => LANG_WEBMAN_OPT_WD_TITLE_LINK,
  17. )),
  18.  
  19. )
  20. ),
  21.  
  22. );
  23.  
  24. }
  25.  
  26. }
\system\languages\ru\controllers\webman\webman.php
  1. <?php
  2.  
  3. define('LANG_WEBMAN_CONTROLLER', 'Хуки-хухуки');
  4.  
  5. define('LANG_WEBMAN_OPT_TAB_WIDGETS', 'Виджеты');
  6.  
  7. define('LANG_WEBMAN_OPT_WD_TITLE_LINK', 'Добавить поле для ссылки под заголовком');
Обновим страницу опций и увидим форму, показанную на заглавной картинке поста. Всё ок.

Ищем событие для добавления поля ссылки в опции виджета

Попробуем выяснить, в каком событии мы можем добавить в форму опций виджета новое поле.
Честно признаюсь, я сначала хотел искать его в коде, поскольку настройки виджетов показываются в модальном окне, где данные отладки не выводятся. Но на счастье в текущей версии расширенной отладки (14.1.1) обнаружилась ошибка, из-за которой в окно настроек виджета выводится отладочная инфа этого окна. Такой «баг» мне понравился, так что в следующей версии сделаю опцию для подобного вывода. А пока смотрим, какие события возникают под формой опций виджета.

Задаём в «Расширенной отладке» подстроку для события ‘widget’ (как пользоваться фильтрами)
После вызова окна настроек виджета на фронтенде видим список интересующих нас событий.
Если сделать такой же фильтр, но по подстроке ‘form’, увидим похожие события
Посмотрим внимательнее на первое событие ‘widget_options_full_form’. Зададим эту строку в качестве имени события в фильтре, включим показ данных событий и вывод трассировки хотя бы на три уровня.
Видим, что в качестве данных события передаётся объект — форма опций виджета «Сейчас онлайн» Object(formWidgetUsersOnlineOptions), окно настроек которого как раз и было мной открыто.

На всякий случай посмотрим, что передаётся в хук в строке /system/controllers/admin/frontend.php (945) (она видна в трассировке).
  1. return cmsEventsManager::hook('widget_options_full_form', $form);
Действительно, в хук передаётся форма опций виджета. Нам повезло с первой попытки, остальные события можно не смотреть.

Поиск этого события без расширенной отладки

Для интересующихся покажу, как искать события «обычным» путём. В любом современном браузере (Хром, Опера, Лиса) открываем «Инструменты разработчика» нажатием Ctrl+Shift+i на нужной странице. Переходим на вкладку «Сеть» (Network), включаем «Не очищать лог» (Preserve log) и кликаем ссылку «Редактировать» в нужном виджете на фронте.
Ищем тип лога ‘document’, подводим мышку к имени объекта и видим строку запроса для него /admin/widgets/edit/6 с параметрами. Следовательно, настройки вызываются через экшен widgets_edit компонента admin. Открываем в PHP-редакторе файл /system/controllers/admin/actions/widgets_edit.php (он, кстати, как раз виден в трассировке в «Расширенной отладке» на скрине выше) и внимательно изучаем его код, заглядывая во все вызываемые методы.

В строке 37 видим метод getWidgetOptionsForm(), находим его в контроллере Админки /system/controllers/admin/frontend.php (строка 752), а уже в нём видим искомый нами вызов хуков 'widget_options_full_form' в строке 945.

Как по мне, так просмотр событий отладкой в несколько кликов гораздо удобнее и проще.

Добавляем поле ссылки в опции виджета

Обработчики хуков обычно находятся в отдельных файлах в папке ‘hooks’ своего компонента. Мы тоже создадим такую паку и добавим в неё файл с именем обрабатываемого события /system/controllers/webman/hooks/widget_options_full_form.php.

Если вы читали документацию по обработке событий, то знаете, что
«Внутри файла хука должен быть определен класс on{Имя компонента}{Название cобытия}, наследуемый от системного класса cmsAction. Название события может состоять из нескольких слов, разделенных знаком подчеркивания. В названии класса эти слова пишутся слитно – каждое с большой буквы.»

Так что делаем в файле класс onWebmanWidgetOptionsFullForm:
  1. <?php
  2.  
  3. class onWebmanWidgetOptionsFullForm extends cmsAction {
  4.  
  5. public function run($form) {
  6.  
  7. // --- Поле ссылки под заголовком виджета ------------------------------
  8.  
  9. if ($this->options['widgets_title_link']) {
  10.  
  11. $form ->addFieldAfter(
  12. 'is_title',
  13. 'basic_options',
  14. new fieldString(
  15. 'options:title_link',
  16. 'title' => LANG_WEBMAN_WD_OPT_TITLE_LINK
  17. )
  18. )
  19. );
  20. }
  21.  
  22. return $form;
  23.  
  24. }
  25.  
  26. }
В метод run($form) приходит форма опций, которую мы видели в отладке. Вот в неё и будем добавлять новое поле.

Сначала проверяем, включена ли опция нашего компонента 'widgets_title_link'.
Помните, мы в контроллере (фронтенде) своего компонента задавали свойство:
  1. protected $useOptions = true;
Оно позволяет обращаться к опциям кратко через $this->options['имя_опции'], вместо их получение методом getOptions() и сохранения в промежуточной переменной.

Если опция 'widgets_title_link' включена, добавляем новое поле типа ‘fieldString’ с именем 'options:title_link' после поля 'is_title' в наборе 'basic_options'. Для поля задаём только его заголовок константой LANG_WEBMAN_WD_OPT_TITLE_LINK.

Обязательно передаём форму $form в следующий хук через оператор return – это стандарт для обработчиков хуков. Кстати, хук работает по последовательной схеме (надеюсь, вы прочитали про работу событий и хуков по ссылке в начале поста?).

Обратите внимание, нам ведь нужно как-то сохранить значение нового поля в базе данных. Можно, конечно, проверять существование поля с нужным именем в таблице виджетов и при его отсутствии тихонечко создавать. Но для упрощения хранения нашего небольшого поля воспользуемся уже существующим массивом опций виджета, добавив в него один элемент. Так и получилось составное имя поля 'options:title_link' ('массив: элемент').

Добавим новую константу LANG_WEBMAN_OPT_WD_TITLE_LINK в языковой файл нашего компонента:
  1. <?php
  2.  
  3. define('LANG_WEBMAN_CONTROLLER', 'Хуки-хухуки');
  4.  
  5. define('LANG_WEBMAN_OPT_TAB_WIDGETS', 'Виджеты');
  6.  
  7. define('LANG_WEBMAN_OPT_WD_TITLE_LINK', 'Добавить поле для ссылки под заголовком');
  8.  
  9. define('LANG_WEBMAN_WD_OPT_TITLE_LINK', 'Ссылка под заголовком виджета');
И последний шаг этого этапа – нужно сказать системе, что наш компонент готов обрабатывать событие 'widget_options_full_form'.
До версии InstantCMS 2.14.1 включительно нужно прописывать массив с обрабатываемыми событиями в файле manifest.php компонента. С версии движка 2.14.2 необходимость в этом отпадёт. Но пока её релиза нет, делаем в корневой папке компонента манифест /system/controllers/webman/manifest.php
  1. <?php
  2.  
  3. return array(
  4.  
  5. 'hooks' => array(
  6.  
  7. 'widget_options_full_form' // Добавление полей в форму опций виджетов
  8. )
  9.  
  10. );
Файл просто возвращает массив с перечнем хуков своего компонента. В нашем случае в нём будет один хук.
Чтобы движок смог обрабатывать это событие из манифеста, зайдём в «Админка – Компоненты — Управление событиями» и разрешим добавить этот хук в БД.
Теперь зайдём в опции любого виджета, например, «Сейчас онлайн» и в новом поле «Ссылка под заголовком виджета» впишем нужную ссылку /users/index/online

Подставляем ссылку под заголовок виджета

Я знаю два способа как это сделать: правильный – шаблоном, и не правильный – ещё одним хуком.

Подставляем ссылку шаблоном

Заголовки виджетов выводятся в шаблонах их обёрток-контейнеров. Помните, в настройках дизайна виджетов есть список «Шаблон контейнера»?
Нас будет интересовать стандартная обёртка, так как в других подставлять ссылку не имеет смысла. Она находится в файле /templates/default/widgets/wrapper.tpl.php для дефолтного шаблона и в аналогичном по соответствующему пути для Модерна.
Создаём её копию в той же папке с подходящим наглядным именем /templates/default/widgets/wrapper_title_link.tpl.php
  1. <?php
  2. /* Копия дефолной обёртки 'wrapper' с добавлением ссылки под заголовком виджета */
  3.  
  4. // Получаем привязку виджета с настройками для дальнейшего использования его опций
  5. $widget_bind = cmsCore::getModel('widgets')->getWidgetBinding($widget['bind_id']);
  6. ?>
  7.  
  8. <div class="widget<?php if ($widget['class_wrap']) { ?> <?php echo $widget['class_wrap']; } ?>" id="widget_wrapper_<?php echo $widget['id']; ?>">
  9. <?php if ($widget['title'] && $is_titles){ ?>
  10. <h4 class="title<?php if ($widget['class_title']) { ?> <?php echo $widget['class_title']; } ?>">
  11. <?php // Добавлена ссылка под заголовком виджета ?>
  12. <?php echo ( isset($widget_bind['options']['title_link']) ? '<a href="'.$widget_bind['options']['title_link'].'">'.$widget['title'].'</a>' : $widget['title'] ); ?>
  13. <?php if (!empty($widget['links'])) { ?>
  14. <div class="links">
  15. <?php $links = string_parse_list($widget['links']); ?>
  16. <?php foreach($links as $link){ ?>
  17. <a href="<?php html((strpos($link['value'], 'http') === 0) ? $link['value'] : href_to($link['value'])); ?>">
  18. <?php html($link['id']); ?>
  19. </a>
  20. <?php } ?>
  21. </div>
  22. <?php } ?>
  23. </h4>
  24. <?php } ?>
  25. <div class="body<?php if ($widget['class']) { ?> <?php echo $widget['class']; } ?>">
  26. <?php echo $widget['body']; ?>
  27. </div>
  28. <?php if(cmsUser::isAdmin()){ ?>
  29. <?php include 'wrap_edit_links.tpl.php'; ?>
  30. <?php } ?>
  31. </div>
В файле всего два изменения, они помечены комментариями. Перед шаблоном мы получаем в переменную $widget_bind настройки виджета. Там в опциях мы сохранили значение нашего нового поля с адресом.
А потом в строке
  1. <?php echo ( isset($widget_bind['options']['title_link']) ? '<a href="'.$widget_bind['options']['title_link'].'">'.$widget['title'].'</a>' : $widget['title'] ); ?>
создаём тег ссылки вокруг заголовка $widget['title'] и подставляем в него ссылку из опций $widget_bind['options']['title_link']

Теперь выберем в настройках дизайна виджета новый шаблон контейнера, обновим страницу с этим виджетом и увидим ссылку под заголовком.
Ссылку раскрасите сами в css-файле своего шаблона.

Этот способ чуть сложнее при обновлении, так как нужно отслеживать изменения в оригинальном шаблоне-обёртке. Но если вы и так делали для своего проекта новый шаблон и меняли обёртки – то это точно ваш способ. Он ничего не испортит в других виджетах и предоставит вам полную свободу.

Подставляем ссылку хуком

Попробуем добиться такого же результата хуком. Поищем подходящее событие. Казалось бы, нужно в отладке вывести в лог виджеты и события, и посмотреть, какие события происходят внутри виджетов. А нетушки… Во-первых, нам нужно подставить ссылку во все виджеты, где она будет задана в настройках, а не в один. А во-вторых, внутри виджетов нет событий для изменения их настроек, что логично.

Поэтому в логе выведем события и виджеты, в фильтр по имени события впишем ‘widget’, а в фильтре по имени виджета впишем название первого виджета на странице (на главной это «Нижнее меню»).
Видим, что перед виджетом есть событие ‘widgets_before_list’, по названию похожее на список виджетов.
Выключим лог виджетов, а в фильтре событий зададим это имя. Также разрешим вывод данных и результатов событий в лог.
В этом событии действительно присутствует список из 18 виджетов текущей страницы (главной на демо) – то, что нам надо.

По аналогии с первым хуком создадим новый файл с именем обрабатываемого события /system/controllers/webman/hooks/widgets_before_list.php.
  1. <?php
  2.  
  3. class onWebmanWidgetsBeforeList extends cmsAction {
  4.  
  5. public function run($widgets_list) {
  6.  
  7. // --- Поле ссылки под заголовком виджета ------------------------------
  8.  
  9. if ($this->options['widgets_title_link']) {
  10.  
  11. foreach ($widgets_list as $pos => $widget) {
  12.  
  13. if ($widget['is_title'] && $widget['title'] && isset($widget['options']['title_link']) ) {
  14.  
  15. $widgets_list[$pos]['title'] = '<a href="'.$widget['options']['title_link'].'">'.$widget['title'].'</a>';
  16.  
  17. }
  18.  
  19. }
  20.  
  21. }
  22.  
  23. return $widgets_list;
  24.  
  25. }
  26.  
  27. }
Имя класса соответствует вышеописанному шаблону из документации.

На входе в методе run($widgets_list) получаем массив $widgets_list – список виджетов страницы. Его же, только с нашими изменениями (если они будут) возвращаем и передаём другим хукам оператором return.

Внутри обработчика проверяем разрешение подстановки ссылки в опции нашего компонента 'widgets_title_link'. И проходя в цикле по всем виджетам списка заменяем заголовок виджета $widget['title'] на html-код с ссылкой под этим заголовком. Замену производим прямо в полученном списке $widgets_list. Естественно, делаем это только в тех виджетах, где задана ссылка $widget['options']['title_link'], а также где срабатывает стандартное условие для вывода заголовка ($widget['is_title'] && $widget['title']) – заголовок включён и задан.

Теперь добавим второй хук 'widgets_before_list' в манифест
  1. <?php
  2.  
  3. return array(
  4.  
  5. 'hooks' => array(
  6.  
  7. 'widget_options_full_form', // Добавление полей в форму опций виджетов
  8.  
  9. 'widgets_before_list', // Изменение списка виджетов
  10. )
  11.  
  12. );
И также разрешим добавить этот хук в БД в «Админка – Компоненты — Управление событиями».
Обновим страницу с виджетами и насладимся результатом – появились ссылки.

Получили простой, универсальный способ, который даже не затрётся при обновлении движка. Но он не «по феншую». Нельзя так принципиально менять переменную, поскольку другие компоненты или шаблоны ожидают, что там будет простой текст без html-кода.
Нам для изучения хуков он подойдёт. Как подойдёт и для тестовых версий сайтов или для очень простых проектов, ведомым одним программистом (и пусть меня закидают помидорами маститые разработчики 😊 ).

Важно! Одновременно два способа использовать нельзя, иначе получите ссылку в ссылке.


Делаем пакет обновления компонента

Если вы правили файлы прямо в проекте своего сайта, что не очень хорошо, то больше ничего делать не нужно. Только выберите способ подстановки ссылки – шаблоном или хуком.

Но если вы делаете свой компонент для хуков в отдельном проекте, как и должно быть, то нужно сделать пакет обновления компонента, чтобы использовать его на ваших сайтах. Естественно, сам компонент для хуков уже должен быть там установлен.

Опять внимательно читаем «Создание пакета дополнения CMS»

Так же, как и для установщика пустого компонента создаём отдельную папку (не в папках проекта или сайта!) webman_update_1.1.0. В ней создаём папку package, в которую с сохранением структуры папок копируем все добавленные или изменённые нами файлы.


Рядом с папкой package создаём файл manifest.ru.ini с описанием нашего пакета:
  1. [info]
  2. title = "Хуки-хухуки"
  3.  
  4. [version]
  5. major = "1"
  6. minor = "1"
  7. build = "0"
  8. date = "20210204"
  9.  
  10. [depends]
  11. core = "2.14.0"
  12. package = "1.0.0"
  13.  
  14. [update]
  15. type = "component"
  16. name = "webman"
  17.  
  18. [author]
  19. name = "WebMan"
  20. url = "/users/WebMan"
  21.  
  22. [description]
  23. text[] = "Добавлена возможность ссылок под заголовками виджетов."
Имя пакета, название и автора, как обычно, меняете на свои.

Изменилась версия с 1.0.0 на 1.1.0. Если доработаете эту же функцию (например, добавите подсказку или стиль к ссылке) или исправите ошибки, версия будет меняться на 1.1.1, 1.1.2, 1.1.3 и т.д. При добавлении нового функционала или принципиальных изменениях можно будет увеличить минорную (вторую слева) цифру 1.2.0 и т.п.

Блок [install] превратился в [update]. В блоке [depends] добавилась зависимость этого обновления от версии установленного компонента 'package'. Она должна быть не ниже "1.0.0"

При необходимости можете создать такие же файлы манифеста и для других языков.

Запаковываем манифест и папку с файлами пакета (всё, что внутри папки ‘webman_update_1.1.0’, без неё самой) в архив webman_update_1.1.0.zip.

Вы ещё помните, что имя webman везде нужно заменить на своё имя компонента с сохранением регистров символов?


Устанавливаем полученный пакет стандартным образом через Админку.
После этого открываем «Компоненты», находим свой компонент, кликом по названию открываем его опции и включаем чекбокс «Добавить поле для ссылки под заголовком».

Обратите внимание! Если выключить новую опцию и пересохранить настройки виджета без поля для ссылки в настройках, то сохранённая ранее ссылка в этом виджете пропадёт и не вернётся даже после включения опции, поле ссылки станет пустым.



Ну как, просто? 😉
Если вы всё поняли и смогли повторить в своём компоненте, значит я не зря всё это расписывал.
Если не уловили принцип или не разобрались в тонкостях кода этого простого примера, тогда, надеюсь, вы прочувствовали, за какие знания и умения вы платите деньги разработчикам компонентов и начнёте больше ценить их труд. 😊
Олег Васильевич я Олег Васильевич я 3 года назад #
Комментарий удален
Олег Васильевич я Олег Васильевич я 3 года назад #
Комментарий удален
+1
WebMan WebMan 3 года назад #
Объяснить работу Двойки лучше, чем это сделали разработчики в " Документации" я не смогу smile
Максимум, на что меня хватит - несколько примеров с картинками, что я и делаю.
В первом своём каменте Вы уже привели ссылку на пост про схему работы движка Двойки, он как раз и показывает как "Всё начинается в index.php...".
+3
WebMan WebMan 3 года назад #
Спасибо за идею! Чуть позже добавлю список своих основных постов, сгруппированных по темам.

Еще от автора

Хуки-хухуки: Исключаем неактивных пользователей из списков
Как иногда начинают свой монолог неопытные стендаперы: «У всех в жизни было такое …
«Расширенная отладка» для InstantCMS 2.14.1 (v.14.1.2) – большое обновление для разработчиков
Новые возможности и удобства, облегчающие разработчикам отладку компонентов и шаблонов.
Использование расширенной отладки. Часть 11. Анализ ошибок 403/404 и редиректов
Одной из неудобных задач при отладке для меня является поиск причины ошибки 403/404.
Используя этот сайт, вы соглашаетесь с тем, что мы используем файлы cookie.