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

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

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

Подготовка

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

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

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

Начнём с простого и уже известного – добавим опцию. У нас уже есть тестовый чекбокс 'is_it_works' в наборе полей 'Тест'. Так что мы просто переименуем этот чекбокс на 'widgets_title_link' и языковые константы поменяем на нужные.
\system\controllers\webman\backend\forms\form_options.php
Код 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
Код 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’ ( как пользоваться фильтрами)
Фильтр событий
После вызова окна настроек виджета на фронтенде видим список интересующих нас событий.
События ‘widget’
Если сделать такой же фильтр, но по подстроке ‘form’, увидим похожие события
События ‘form’
Посмотрим внимательнее на первое событие ‘widget_options_full_form’. Зададим эту строку в качестве имени события в фильтре, включим показ данных событий и вывод трассировки хотя бы на три уровня.
Фильтр событий
Видим, что в качестве данных события передаётся объект - форма опций виджета «Сейчас онлайн» Object(formWidgetUsersOnlineOptions), окно настроек которого как раз и было мной открыто.

На всякий случай посмотрим, что передаётся в хук в строке /system/controllers/admin/frontend.php (945) (она видна в трассировке).
Код PHP:
  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:
Код PHP:
  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'.
Помните, мы в контроллере (фронтенде) своего компонента задавали свойство:
Код PHP:
  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 в языковой файл нашего компонента:
Константы
И последний шаг этого этапа – нужно сказать системе, что наш компонент готов обрабатывать событие 'widget_options_full_form'.
До версии InstantCMS 2.14.1 включительно нужно прописывать массив с обрабатываемыми событиями в файле manifest.php компонента. С версии движка 2.14.2 необходимость в этом отпадёт. Но пока её релиза нет, делаем в корневой папке компонента манифест /system/controllers/webman/manifest.php
Код 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
Новая обёртка
В файле всего два изменения, они помечены комментариями. Перед шаблоном мы получаем в переменную $widget_bind настройки виджета. Там в опциях мы сохранили значение нашего нового поля с адресом.
А потом в строке
Код PHP:
  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.
Код 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' в манифест
Код PHP:
  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-кода.
Нам для изучения хуков он подойдёт. Как подойдёт и для тестовых версий сайтов или для очень простых проектов, ведомым одним программистом (и пусть меня закидают помидорами маститые разработчики smile ).

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

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

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

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

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

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


Рядом с папкой package создаём файл manifest.ru.ini с описанием нашего пакета:
Код 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 = "http://www.instantcms.ru/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 везде нужно заменить на своё имя компонента с сохранением регистров символов?

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

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


Ну как, просто? joke
Если вы всё поняли и смогли повторить в своём компоненте, значит я не зря всё это расписывал.
Если не уловили принцип или не разобрались в тонкостях кода этого простого примера, тогда, надеюсь, вы прочувствовали, за какие знания и умения вы платите деньги разработчикам компонентов и начнёте больше ценить их труд. smile
Хуки-хухуки: Как создать свой компонент для хуков? | Использование расширенной отладки. Часть 10. Табличные контрольные точки
Комментарии (4)
Олег Васильевич я 5 февраля 2021 в 21:52 +1
Webman, нет возможности как-то объединить опубликованных вами ранее записей типа этой с новыми? Как-то теряется ниточка последовательности...
Спасибо!
Олег Васильевич я 5 февраля 2021 в 21:56 +1
...Может рубрику отдельную создатите, где и объясните нам устройство двойки "на пальцах"? Типа "Всё начинается в index.php..."
WebMan 6 февраля 2021 в 10:59 +1
Объяснить работу Двойки лучше, чем это сделали разработчики в " Документации" я не смогу smile
Максимум, на что меня хватит - несколько примеров с картинками, что я и делаю.
В первом своём каменте Вы уже привели ссылку на пост про схему работы движка Двойки, он как раз и показывает как "Всё начинается в index.php...".
WebMan 6 февраля 2021 в 10:55 +3
Спасибо за идею! Чуть позже добавлю список своих основных постов, сгруппированных по темам.