info@1px.su

Статья о том, как писать модули в Modx Evolution

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

Поэтому летать будем по пачке "Беломора".

Задача

Простой модуль для редактирования документов. Оформление стандартное, в стилях админки. Модуль мультиязычный, шаблонизируемый, расширяемый под будущие задачи. У нас получится "болванка", которую можно будет использовать дальше.

Главная страница модуля. Список товаров из нужного раздела, у каждого товара есть заголовок, аннотация и редактирование. Тв-параметры мы брать не будем, это всё же модуль для обучения.

Процесс редактирования.

Дополнительная вкладка. Просто для примера.

Структура модуля

1. Лезем в assets/modules и создаём папки и пустые файлы.


Основная папка модуля - contentEditor. В ней всего 1 файл - это core.php. Ядро и основной функционал нашего будущего модуля. В css будут стили, в templates шаблоны, в lang языковые версии. 

Начальный шаблон модуля

Файлы шаблонов для модуля -- это просто html файлы. Разумеется, раз это шаблоны, у них будет как статичная часть, так и динамические блоки, в которые мы и будем вставлять плейсхолдеры, которые сами же и зададим.

Создаём файл main.html. У всех файлов указывать кодировку utf-8 (без BOM)!

Что нам нужно сделать? Давайте ещё раз глянем на стартовую страницу нашего будущего модуля.

str1.png

Начнём. Для того, чтобы мы могли использовать стили и некоторые возможности Evo, пишем в секции head следующее:

  1. <head>
  2. <meta content="text/html; charset=" http-equiv="Content-Type">
  3. <title></title>
  4. <link rel="stylesheet" type="text/css" href="media/style//style.css">
  5. <link rel="stylesheet" type="text/css" href="assets/modules/contentEditor/css/main.css">
  6. <script type="text/javascript" src="media/script/mootools/mootools.js" ></script>
  7. <script type="text/javascript" src="media/script/mootools/moodx.js"></script>
  8. <script type="text/javascript" src="media/script/tabpane.js"></script>
  9. </head>

Как видите, это не совсем чистый html. Мы вставили в некоторые атрибуты плейсхолдеры. Давайте разбираться. В первой строке вставлен  - это кодировка сайта. Эво автоматически заменит её на ту, что выставлена в настройках.

Дальше 2 плейсхолдера   и . Первый отвечает за путь к файлам темы админ-панели. Второй - урл сайта. Т.е. мы не задаём жёсткий путь к файлу стилей, а конструируем его.

Зачем мы это сделали? Чтобы наш модуль брал стили из той темы админки, которая установлена в настройках сайта в данный момент.

Для примера я меняю тему на старую, и вот так внешне выглядит модуль, в стиле старой доброй зелёненькой админки из версии 1.15.

Едем дальше. Как мы видим, наверху модуля у нас есть заголовок, аннотация и кнопка "Обновить". Давайте их сделаем. Открываем body и пишем:

  1. <h1></h1>
  2. <div id="actions">
  3. <ul class="actionButtons">
  4. <li id="Button1">
  5. <a href="#" onclick="document.location.href=document.location.href;">
  6. <img src="media/style//images/icons/refresh.png">
  7. </a>
  8. </li>
  9. </ul>
  10. </div>

Что же такое мы вставили в <h1>?  - это наш будущий плейсхолдер, где мы будем хранить название модуля. Я назвал его так, вы можете придумать своё название, главное, не забудьте. Эти самодельные плейсхолдеры мы будем потом переводить на 2 языка, чтобы модуль был мультиязычным и расширяемым.

Ниже вставляем код для кнопки "Обновить". Это более-менее стандартная секция для кнопок действия в модулях. По-умолчанию она крепится справа-сверху и стилизуется под админку. Если присмотреться, мы опять видим плейсхолдер папки в адресе картинки. Т.е. меняем тему -- меняется рисунок у кнопки. В плейсхолдер  будет записан текст на кнопке. Запоминаем это. Действие на клик по кнопке - вполне обычный ява-скрипт для перезагрузки. Кнопка "Обновить" готова.

Hint: Если полазать в папке media/style/папка темы/images/icons то можно найти кучу иконок для своих кнопок, которые вы также можете установить, создав новый <li> с похожей разметкой.

Делаем тело страницы. Посмотрите на макет. Нам нужны две вкладки. В первой из них будет таблица с ячейкой заголовков и содержимым. Во второй только текст.

  1. <div class="sectionBody">
  2. <p></p>
  3. <div class="tab-pane" id="tabPanel">
  4. <script type="text/javascript">
  5. mypanel = new WebFXTabPane(document.getElementById("tabPanel"), true);
  6. </script>
  7. <div class="tab-page" id="startTab">
  8. <h2 class="tab"></h2>
  9. <script type="text/javascript">mypanel.addTabPage(document.getElementById("startTab"));</script>
  10. <div>
  11. <table class="grid">
  12. <thead>
  13.    <tr>
  14.        <td class="gridHeader"></td>
  15.        <td class="gridHeader"></td>
  16.        <td class="gridHeader"></td>
  17.        <td class="gridHeader"></td>
  18.    </tr>
  19. </thead>
  20. <tbody>
  21.    
  22. </tbody>
  23. </table>
  24. </div>
  25. <p></p>
  26. </div>
  27. <div class="tab-page" id="startTab2">
  28. <h2 class="tab"></h2>
  29. <script type="text/javascript">mypanel.addTabPage( document.getElementById("startTab2"));</script>
  30. <div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>

Довольно непростой кусок кода на первый взгляд.
Сразу же идёт плейсхолдер - это будет описание модуля.
Класс sectionBody -- это стиль Эво и используется он для тела модуля.

Создадим разметку табов. Она начинается созданием слоя-оболочки с классом tab-pane и идентификатором tabPanel. Идентификатор может быть произвольным. Сразу же после открывающего тега запускаем стандартный скрипт Эво для панелей. Это вызов функции WebFXTabPane, которой мы передаём в качестве аргумента id созданного слоя-оболочки.

Сделаем вкладки. Каждая из них должна иметь уникальный id и класс tab-page. Сразу же внутри вкладки нужно разместить заголовок панели <h2 class="tab"></h2>. Что такое вы уже догадались? Правильно, это название панели, которое мы позже зададим в файлах языка.

Тут же промотайте код до 27-й линии. Мы создаём 2-ю вкладку, <div class="tab-page" id="startTab2">. Всё по аналогии с первой, за исключением id. Напомню, у каждой вкладки он должен быть уникален. Для того, чтобы вкладки работали и переключались, нужно их добавить в обработчик.

За это отвечает строка  скрипта mypanel.addTabPage(document.getElementById("startTab"));

Строку надо вызывать в <script> внутри каждого из табов. Как видите, в первом табе мы использовали id первого таба, во втором - второго.

Сразу закончим с табом номер два, так как он попроще. Единственное содержимое там это плейсхолдер . Да, это будет некий статичный текст, который также будет подгружен сюда позже.

Таб номер один. Нужно создать таблицу для того, чтобы в ней динамически выводить содержимое. Посмотрите на код, комментировать тут нечего, кроме, разве что классов - это стандартные классы Эво для таблицы. Плейсхолдеры для заголовков типа table_id и table_header аналогичны остальным. Под таблицей, как мы помним по макету, у нас разместится небольшой абзац текста. Давайте его тоже сделаем изменяемым. Я назвал плейсхолдер и заключил его в абзац.

В конце концов мы добрались до плейсхолдера . Здесь разместится результат работы скрипта core.php. Давайте перейдём к нему.

Ядро модуля

Начинаем делать самое интересное, ядро модуля. Открываем core.php, проверяем кодировку. Должна быть utf-8 без BOM.

Первой же строкой после открывающего тега php мы проверим, может ли пользователь открывать этот файл.

  1. if(IN_MANAGER_MODE!='true' && !$modx->hasPermission('exec_module')) die('ERROR');

Зададим несколько переменных, которые нам понадобятся в работе со скриптом

  1. $Template=new Template;//новый класс. О нём позже
  2. $bigAction = $_GET['a'];//текущее значение аргумента a из урла
  3. $moduleId = $_GET['id'];//айди модуля
  4. $FullTableName = $modx->getFullTableName('site_content');//полное имя таблицы контента

Пишем класс для шаблонизации.

  1. class Template{
  2. public $lang;
  3. function __construct(){
  4. global $modx;
  5. $lang = $modx->config['manager_language'];
  6. if (file_exists( dirname(__FILE__) . '/lang/'.$lang.'.php')){
  7. include_once(dirname(__FILE__) . '/lang/'.$lang.'.php');
  8. } else {
  9. include_once(dirname(__FILE__) . '/lang/english.php');
  10. }
  11. $this->lang = $_field;
  12. }
  13. function getTpl($file){
  14. ob_start();
  15. include($file);
  16. $tpl = ob_get_contents();
  17. ob_end_clean();
  18. return $tpl;
  19. }
  20. static function parseTemplate($tpl,$field){//вот он парсер
  21. foreach($field as $key=>$value) $tpl = str_replace('',$value,$tpl);
  22. return $tpl;
  23. }
  24. }

Код достаточно сложен для понимания сходу.

Вкратце - он подключает языковые файлы, исходя из того, какая версия языка выбрана в админке, инициализирует переменную-массив lang. В этот массив мы будем писать наши плейсхолдеры, кстати. Далее он парсит переданный ему шаблон, ищет соответствия между плейсхолдером в шаблоне и значением в lang-файле и возвращает результат -- собранную страницу. Как пользоваться классом, мы рассмотрим чуть ниже. Пока же просто скопируйте его в core.php

Теперь начинаем думать над функционалом. Модуль должен делать 2 вещи. Первая - показать нам список товаров. Вторая - редактировать выбранный товар. Конечно, можно заморочиться с серьёзной шаблонизацией, роутингом и классами для каждого действия, но задача сейчас проще. Поэтому все наши действия заворачиваем в switch-case и получаем вот такой большой скрипт.

  1. switch($_REQUEST['action']){
  2. default: //    Действия при загрузке модуля
  3. $section=$params['sectionId']; //    Получаем из конфига id раздела
  4. $result = $modx->db->select('id,pagetitle,introtext', $FullTableName, 'parent='.$section, '', 30);
  5. if($modx->db->getRecordCount($result)>= 1){
  6.   while($row = $modx->db->getRow( $result )){
  7.       if($class){$class="gridAltItem";}else{$class="gridItem";} //    Оформление ячеек, "зёбра"           
  8.       $Template->lang['phpwork'] .='<tr class="'.$class.'">';
  9.       $Template->lang['phpwork'] .='<td >'.$row["id"].'</td>';
  10.       $Template->lang['phpwork'] .='<td>'.$row["pagetitle"].'</td>';
  11.       $Template->lang['phpwork'] .='<td >'.$row["introtext"].'</td>';
  12.       $Template->lang['phpwork'] .='<td ><a href="index.php?&a=' . $bigAction. '&id='.$moduleId . '&editDoc='. $row['id'].'&action=edit" data-id="'.$row["id"].'">' . $Template->lang['edit'] . '</a></td>';
  13.       $Template->lang['phpwork'] .='</tr>';
  14.   }
  15. }
  16. else{
  17.   $Template->lang['phpwork'] = '';
  18. }
  19. $tpl = Template::parseTemplate($Template->getTpl(dirname( __FILE__ ).'/templates/main.html'),$modx->config);
  20. $tpl = Template::parseTemplate($tpl ,$Template->lang);
  21. echo $tpl;
  22. break;
  23. case 'edit':
  24. if($_POST){
  25.   $fields = array(
  26.   "pagetitle" => $modx->db->escape($_POST["pagetitle"]),
  27.   "introtext" => $modx->db->escape($_POST["introtext"])
  28.   );
  29.   $result = $modx->db->update($fields, $FullTableName, "id=" . $_GET['editDoc']);
  30.   if($result){
  31.       $Template->lang['phpwork'] = $Template->lang['save_success'];
  32.   }
  33.   else{
  34.       $Template->lang['phpwork'] = $Template->lang['save_error'];
  35.   }
  36. }
  37. else{
  38.   $result = $modx->db->select('id,pagetitle,introtext', $FullTableName, 'id='.$modx->db->escape($_GET['editDoc']));
  39.   if($modx->db->getRecordCount($result)>= 1){
  40.       while($row = $modx->db->getRow( $result )){
  41.           if($class){$class="gridAltItem";}else{$class="gridItem";} //    Оформление ячеек, "зёбра"           
  42.           $Template->lang['phpwork'] .='<tr class="'.$class.'">';
  43.           $Template->lang['phpwork'] .='<td>'.$Template->lang['header'].'</td>';
  44.           $Template->lang['phpwork'] .='<td><input name="pagetitle" type="text" maxlength="255" value="'.$row["pagetitle"].'" class="inputBox" onchange="documentDirty=true;" spellcheck="true"></td>';
  45.           $Template->lang['phpwork'] .='</tr>';
  46.           $Template->lang['phpwork'] .='<tr class="'.$class.'">';
  47.           $Template->lang['phpwork'] .='<td>'.$Template->lang['table_header2'].'</td>';
  48.           $Template->lang['phpwork'] .='<td><textarea id="introtext" name="introtext" class="inputBox" rows="3" cols="" onchange="documentDirty=true;">'.$row["introtext"].'</textarea></td>';
  49.           $Template->lang['phpwork'] .='</tr>';
  50.       }
  51.   }
  52. }
  53. $tpl = Template::parseTemplate($Template->getTpl(dirname( __FILE__ ).'/templates/edit.html'),$modx->config);
  54. $tpl = Template::parseTemplate($tpl ,$Template->lang);
  55. echo $tpl;
  56. break;
  57. }

Посмотрите неспеша, при всей своей громоздкости этот код более чем понятен. Итак, мы имеем разделение на 2 действия в case. В самом начале мы ловим переданный нам $_REQUEST['action']

Действие defaultпроисходит по-умолчанию при загрузке модуля, когда никакой action нам не передан.

Здесь может возникнуть вопрос, что за $params['sectionId'] такая. Если вы лазали в конфигурации модулей, то могли увидеть там похожую картину:

Это параметры модуля. Скрипт может их получить в любой момент в  массиве $params. Как задать параметр? Для начала надо создать новый модуль. Перейти во вкладку "Свойства" и там задать их в формате json. Нам нужен только 1 параметр, sectionId. В него мы будем писать, из какого раздела брать документы. Заполняем свойства { "sectionId": [    {      "label": "ID родителя",      "type": "integer",      "value": "2",      "default": "2",      "desc": ""    }  ] }

Всё. Теперь в тело модуля пишите подключение скрипта ядра.

include_once('../assets/modules/contentEditor/core.php');

Заполняйте название и описание и можно смело обновлять страницу админки, модуль будет установлен. А мы продолжим разбираться со скриптом.

Как видите, в действии по-умолчанию мы делаем запрос к базе, разбираем его и в цикле присваиваем переменной lang['phpwork'] результат работы, строчка за строчкой. А phpwork - ничего не напоминает? Это наш плейсхолдер, заданный в шаблоне. Т.е. мы будем выводить на его месте результаты работы скрипта.

А как будем это делать, скажут эти 2 строчки вызова класса.

  1. $tpl = Template::parseTemplate($Template->getTpl(dirname( __FILE__ ).'/templates/main.html'),$modx->config);
  2. $tpl = Template::parseTemplate($tpl ,$Template->lang);

В целом они более-менее понятны. Берём файл, созданный нами недавно и заменяем в нём все плейсхолдеры на их значения из соответствующих переменных.

В действии edit мы проверяем, пришёл ли пост-запрос. Если да, то обновляем содержимое полей в базе и выводим отчёт о работе, либо положительный, либо отрицательный. Если запрос не пришёл, рисуем форму. Ловим переданный нам параметр editDoc, делаем запрос, отображаем поля для редактирования и текущие значения pagetitle и introtext.

Пора сделать языковой файл. Если вы обратили внимание на код класса, то могли заметить, что подключение языка происходит по вот такой схеме, include_once(dirname(__FILE__) . '/lang/'.$lang.'.php') где $lang это текущий язык системы, взятый из конфига Эво.

Руководствуясь этим, создаём 2 файла в папке lang: russian-UTF8.php и english.php

Приведу для примера русский файл. Любой файл любого другого языка абсолютно такой же, за исключением, разумеется, перевода.

  1. <?php
  2. $_field['store_name'] = "Редактор товаров";
  3. $_field['module_description'] = "<p>Модуль редактирования товаров.</p>";
  4. $_field['close'] = "Закрыть";
  5. $_field['edit'] = "Редактировать";
  6. $_field['refresh'] = "Обновить";
  7. $_field['header'] = "Заголовок";
  8. $_field['save'] = "Сохранить";
  9. $_field['table_id'] = "id";
  10. $_field['table_header'] = "Заголовок";
  11. $_field['table_header2'] = "Аннотация";
  12. $_field['table_action'] = "Действия:";
  13. $_field['tab1_header']='Товары';
  14. $_field['tab1_description']='Раздел для управления вы можете указать в конфигурации модуля ("Модули" - "Управление модулями")';
  15. $_field['tab2_header']='О модуле';
  16. $_field['tab1_text']='Тестовый модуль для просмотра и редактирования товаров';
  17. $_field['save_success']='Сохранили';
  18. $_field['save_error']='Ошибка сохранения';
  19. ?>

Как видите, внутри массива $_field мы создали элементы массива, ключи которых полностью совпадают с теми плейсхолдерами, которые заданы в шаблоне main.html.

Попробуйте запустить модуль. У вас должны работать табы, отображаться товары. Но пока что не работает редактирование. Давайте это исправим.

Посмотрите внимательно на вызов парсера при действии edit. Мы вызываем практически всё точно так же, за исключением шаблона. Для редактирования применим новый шаблон, edit.html. Создайте такой файл в папке templates

Вот его полный листинг:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
  5.     <title></title>
  6.     <link rel="stylesheet" type="text/css" href="media/style//style.css">
  7.     <link rel="stylesheet" type="text/css" href="/assets/modules/contentEditor/css/main.css">
  8.     <script type="text/javascript" src="media/script/mootools/mootools.js" ></script>
  9.     <script type="text/javascript" src="media/script/mootools/moodx.js"></script>
  10.     <script type="text/javascript" src="media/script/tabpane.js"></script>
  11.     <script>
  12.     function postForm(){
  13.         document.frm.submit();
  14.     }
  15.     </script>
  16. </head>
  17. <body>
  18.     <h1></h1>
  19.     <div id="actions">
  20.         <ul class="actionButtons">
  21.             <li>
  22.                 <a href="index.php?a=112&amp;id=&lt;?php echo $_GET['id'];?>" class="primary" id="save" onclick="postForm();return false;">
  23.                 <img alt="icons_save" src="media/style/MOD_Anytheme/images/icons/save.png"></a>
  24.             </li>
  25.             <li>
  26.                 <a href="index.php?a=112&amp;id=&lt;?php echo $_GET['id'];?>">
  27.                     <img src="media/style//images/icons/stop.png">
  28.                 </a>
  29.             </li>
  30.         </ul>
  31.     </div>
  32.     <div class="sectionBody">
  33.         <div class="tab-pane" id="cePanel">
  34.             <script type="text/javascript">
  35.                 mypanel = new WebFXTabPane(document.getElementById("cePanel"), true );
  36.             </script>
  37.             <div class="tab-page" id="startTab">
  38.                 <h2 class="tab"></h2>
  39.                 <script type="text/javascript">mypanel.addTabPage(document.getElementById("startTab"));</script>
  40.                 <div>
  41.                     <form name="frm" class="content" method="post" enctype="multipart/form-data">
  42.                         <table class="grid">
  43.                             <tbody>
  44.                                 
  45.                             </tbody>
  46.                         </table>
  47.                     </form>
  48.                 </div>
  49.             </div>
  50.         </div>
  51.     </div>
  52. </body>
  53. </html>

Как видите, он очень похож на main.html. Но есть ньюансы. Во-первых мы задали функцию postForm, которая при вызове отправит содержимое формы с id=frm. Во-вторых, мы задали новые кнопки в панели actionButtons. Кнопка "Сохранить" как раз будет вызывать функцию и отправлять форму, а кнопка "Закрыть" просто переадресует нас на главную страницу модуля, что равносильно закрытию страницы редактирования.

Дальше никаких изменений нет, и в phpwork подставляется содержимое секции case 'edit': из файла core.php.

Конечно, в этот модуль можно было добавить ТВ-параметры, аякс-редактирование и многие другие интересные вещи, однако, статья получилась и без того объёмная.

Статья была написана для портала modx.ru