MODX - первое знакомство

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

Ну, думаю, если людям надо, изучу за пару недель и сделаю. Ответа я так и не дождался. И ради интереса решил глянуть, что за зверь такой этот MODX. И поразился его навороченностью и сложностью. Это далеко не Wordpress и ему подобные...Хотя и написано на главном сайте, что можно быстро создать сайт без знания PHP, jS и mySQL. Может и можно, если только уж очень простой

Короче - это вещь для профи.

Приходит понимание, что я приорел неисчерпаемый источник пищи для ума).

И этот блог я запустил больше для освоения MODX.

Надо сразу отметить, что вменяемая документация только на английском.

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

Сразу понравилось удобство работы прямо в менеджере. Поэтому я уж забыл про локальный apache и десктопные редакторы кода*. Это все легко устанавливается в MODX. Первоначально в MODX ничего нет, кроме менеджера ресурсов и управления системой. Все сам себе устанавливаешь. Это очень просто. Есть поиск дополнений с аннотациями.

Я сразу установил очень удобный редактор кода,WYSIWYG-редактор и LESS. Загрузил Bootstrap.

Теперь закончились муки с CSS. Ну Less и Bootstrap работают вместе.

Причем про Less я узнал разбирая примеры по MODX. Крутейшая штука. Это процессор CSS. Суть: в редакторе кода пишешь специальные инструкции или просто пишешь CSS в различных файлах. А less все включенные файлы объединяет в один сплошной компактный файл обычного CSS. Вся прелесть в инструкциях. Часто называемых примесями. Широко используются переменные, вычисляемые выражения. Типа сделать темнее цвет. Или больше на 30%. Или применить для элемента стиль от другого и добавить свои свойства.

В инете полно сайтов для преобразования файлов инструкций в CSS. Есть процессоры Less на javaScript. Т.е. во время отладки скрипт при каждой загрузки генерит CSS из инструкций less. А когда все готово-подключают сгенерированный CSS и отключают jS-процессор. Немного геморойно... Но не в MODX!

В MODX все очень удобно. Весь фокус в кэшировании работы шаблонизатора ресурсов. На время отладки отключаешь кэш, и при каждой загрузке страницы процессор генерит CSS. И в конце просто включаешь кэш. И теперь будет загружаться CSS из кэша. Песня...

Ну а Bootstrap - это как-раз сборка инструкций для LESS, есть еще и немного javaScripta для различных эффектов. На jQuery. В итоге получаем удобный набор стилей и активного содержания. Т.е. самому уж не надо страдать в муках творчества с оформлением и можно больше заниматься самим сайтом.

Прочие особенности следующие, как я понял на сей момент:

  • Все объекты-ресурсы(страницы и прочее) хранятся в таблицах базы(необязательно mySQL). Но можно создать "статический ресурс", это просто внешний файл.
  • Для манипуляции с объектами используется расширение PDO xPDO(PDO - абстракция PHP для работы с базой(mySQL и прочими))
  • Ресурсы(они же страницы сайта) содержат кроме HTML специальные тэги типа [[...]], которые вызывают куски PHP("сниппеты") , просто куски HTML("чанки") или переменные системы, ссылки на ресурс, поля текущего ресурса, переменные после работы сниппета("плейсхолдеры")
  • К ресурсу можно прикрепить шаблон, содержаший общую схему страницы, к шаблону-привязать переменные шаблона(TV), коих можно создать любое количество.
  • Ресурс можно сделать не кэшируемым, выключенным. Назначить пунк меню и прочее...
  • На стороне менеджера(backend) используется modX. Это расширение extJS(фреймворк javaScript для web-приложений). Тоже впервые с ним столкнулся.
  • Многие дополнения используют, если нужен jS, jQuery(фреймворк javaScript)
  • "Прямо из коробки" присутствуем мощная система разграничений доступа и управления пользователями. Сам еще не до конца разобрался с ней.
  • Ну и много еще чего)))...

По мере освоения xPDO становится ясно, что многие установленные дополнения не нужны. Чаще эффективнее самому выбирать ресурсы и поля из базы. Ну это если поработал уже с PHP и mySQL. Практически все дополнения следует рекомендациям MODX и отделяют PHP от HTML**. Т.е. Весь PHP спрятан в сниппетах, а HTML создается чанками. Общая схема такая. Допустим PHP берет несколько строк из базы. Строки таблиц базы тут принято называть объектами. И при обработке строки(объекта) сниппет устанавливает 'плейсхолдеры'(переменные). После обработки каждой строки сниппет запускает свой чанк, а там шаблонизатор подставляет плейсхолдеры в теги(например: [[+name]]).

Контекст менеджера (Backend)

Модификация менеджера под себя очень непростая. Поначалу шокирует сложностью.

После "лобового" использования mySQL, xPDO воспринимается немного запутанным. Порядок применения может быть следующий:

  • Создается класс таблицы.
    • Содержит(когда я пробовал создать):
    • Указатель на сам объект modx ($modx)
    • Конфигурацию, где одни пути ($config)
    • Конструктор, в котором инициализация this->modx и $config. И добавляется пакет(создается объект xPDO) в modx( $this->modx->addPackage('myClassItem',$this->config['modelPath']); )
    • public function getChunk($name,$properties = array())
    • private function _getTplChunk($name,$postfix = '.chunk.tpl')
  • Создается XML-схема базы, где описываются название таблицы, тип база,все поля, зависимости(внешние ключи) и т.п.
  • Пишется скрипт парсера(генератора). Есть шаблон, просто надо поменять местами пути, наименования. Можно функцию универсальную создать.
  • Запускается скрипт. В результате создаются сами таблицы, классы для xPDO, PHP map-файлы по своему представляющие схему таблиц. И еще несколько файлов.

Это, конечно, намного сложнее, чем просто открыть таблицу в PHP. Ну это компенсируется уменьшением кода потом. Так открыть таблицу счетов(она же объект) и сразу получить строку с id=43 можно так : $bill = $xpdo->getObject('BILL',43). А массив строк с товарами так: $stores = $bill->getMany('STORS'). Очевидно, что стоит вначале помучаться))).

Причем сама XML-схема нужна только для генерации объектов xPDO. И больше нигде не используется, что как-то странно.Может, конечно, я чего-то еще не понял.

После того как создан класс таблицы и все объекты xPDO, можно их использовать в сниппетах(кусках PHP):

Создаем экземпляр класса:
$myClassInst = $modx->getService('myClass','MyClass',$modx->getOption(core_path,null,core_path.'components/myClass/').'model/myClass/',$scriptProperties); - вызов класса

xPDO:
$c = $modx->newQuery('MyClass');
$c->sortby($sort,$dir);
$myclasses = $modx->getCollection('MyClass',$c);

Вывод HTML:
$output = '';
foreach ($myclasses as $row) {
$colArray = $row->toArray();
$output .= $myClassInst->getChunk($tpl,$colArray);
}
return $output;

С путями в MODX особая возня. Одни и те же прописываются и в системных переменных, в пространствах имен, в классах и в передаваемых параметрах. По этой причине чуть ли не половину кода в классах и сниппетах занимают каскады из функции путь=getOption(переменная, там_где_ее_пытаются_найти, то_что_берут_если_не_нашли). Может оно и хорошо, придает гибкость. Но как-то утомляет при написании.



При привязке пользовательского модуля к пункту меню надо учитывать следующие моменты:

  • Для модуля создается пространство имен. Например: "MyModul". И в пространстве имен прописываются каталоги ядра и активов.
  • В каталоге ядра в каталоге "controllers" создается файл контроллера. Например: если в настройке кнопки меню указано действие "index", то имя котроллера будет "index.class.php"
    • В котроллере имя класса="Имя пространства имен"+"IndexManagerController". В нашем случае:"MyModulIndexManagerController". Иначе класс не создастся.
    • В созданном классе обязательны следующие public функции:
      • initialize()
      • getLanguageTopics()
      • checkPermissions()
      • process(array $scriptProperties)
      • getPageTitle()
      • loadCustomCssJs()
      • getTemplateFile()
  • Для ajax-загрузки содержимого(таблицы) запрашивается файл ..../assets/component/MyModul/connector.php, и из него вызывается (неявно и опосредованно, трудно отследить всю цепь)..../core/processors/mgr/MyModul/getlist.class.php . Естественно, что можно назначить свои функции.

Вообще несколько напрягает "подмена" имен файлов. В последнем случае для ajax-загрузки передается "action=mgr/MyModul/getlist", а вызывается "mgr/MyModul/getlist.class.php". А до этого уже упопинал: назначается обработчиком "index", а вызывается "index.class.php". Ну может есть в этом свой смысл.

Переменные шаблона(TV)

Пожалуй это самые важные компоненты в бэкенде. Ибо все нестандартное(кастомное) можно впихнуть в TV(Template Variable)

Это по сути расширение ресурсов(страниц). Но сделано как-то странновато. Судя из названия, TV прикрепляются к шаблону. Нельзя непосредственно прикрепить к странице. А шаблон уже прикрепляется к странице(ресурсу). Но самое главное, что значение TV прикрепляется именно к ресурсу(странице). Реализовано не совсем очевидно. Вот задействованные таблицы:

  • site_templates - шаблоны страниц
  • site_tmplvars - сами TV. Тут имена, свойства ввода-вывода, типы, значения по умолчанию и прочее. Нет внешних ключей. Т.е. ни к кому не привязан. А к шаблонам привязывается через "посредника" - site_tmplvar_templates
  • site_tmplvar_contentvalues - тут значения TV(поле 'value'). Поле "tmplvarid" - внешний ключ site_tmplvars.id. Поле contentid - внешний ключ к site_content.id(т.е. к таблице ресурсов(страниц)).
  • site_tmplvar_templates - "прокладка" между шаблонами страниц(site_templates) и TV(site_tmplvars). Таким образом реализуется отношение "многие ко многим". Каждый TV может быть привязан к многим шаблонам, и в то же время к каждому шаблону могут быть привязаны несколько TV. Имеет два внешних ключа: tmplvarid к site_tmplvars и templateid к site_template:id

Чтобы считать значение TV(по имени) страницы, надо сделать запрос к site_tmplvars по имени, по site_tmplvars:id в site_tmplvar_contentvalues найти нужное значение. И спрашивается причем тут шаблон страницы? Мне как-то нужно было выбрать все страницы с определенным значением TV. Так же в site_tmplvars по имени ищется site_tmplvars:id, по нему в site_tmplvar_contentvalues выбираются все site_content.id. И опять site_templates(шаблоны страниц) не при делах. Получается, что можно было обойтись вообще без привязки TV к шаблонам. Хотя, наверное, это сделано чисто для удобства назначения набора TV сразу при установке шаблона страницы. А шаблон для новых страниц можно назначить по умолчанию в настройках системы. На деле получается двойная привязка страниц к TV: через шаблоны(только к именам) и непосредственно через страницы(только к значениям). И чтобы перебрать все TV со значениями для конкретной страницы(ресурса) надо через шаблон узнать список привязанных TV, а по ним уже выбрать все значения используя id ресурса. Но самое удивительное, что и тут можно обойтись без шаблонов. Все можно выгрести из site_tmplvar_contentvalues. По нужному contentid выбрать все значения и tmplvarid, а по последним из site_tmplvars - имена TV. Но через xPDO все это делается намного проще. Там все внешние ключи прописаны. Есть просто функция getTVValue("имя TV") для объекта ресурса.

Использование очень простое.

  • Элементы->Доп. поля->Новое
  • Присваиваем имя
  • Назначаем шаблон страницы(можно несколько)
  • Выбираем параметры ввода и вывода
  • Сохраняем
  • Теперь на страницах с определенным шаблоном будет доступно это доп. поле

На первый взгляд ничего особенного. Но вся мощь в создании своих типов ввода и вывода. В инете много примеров. Например можно сразу загрузить фотоальбом на одну страницу. И назначить различные параметры отображения. Причем если просто использовать дополнение Gallery, то чтобы отобразить на страницы альбом, нужно сначала его создать, а потом уже назначить в TV страницы. Это не соовсем удобно. А с помощью создания своего типа ввода, можно сразу при назначении TV загрузить альбом, при этом ID альбома и каталог загрузки автоматом назначится. Вот пример реализации

Ну а определив свой тип вывода, можно сильно облегчить работу верстальщика или свою. Все значения TV храняться в поле "value" таблицы "site_tmplvar_contentvalues". Причем простые типы - просто как текст. А сложные, состоящие из нескольких полей - в формате JSON.

Звучит, конечно, заманчиво). Но когда я впервые глянул на примеры реализации, то реально волосы встали дыбом. Пришлось почитать родную документацию по шаблонизатору Smarty(благо она на русском есть), на английском по ExtJs(на нем весь интерфейс бэкенда) и пробежаться по классам MODX(класс PHP) и MODx(объект JS). После этого стал немного понимать. Причем, как оказалось, по многим вещам документации просто нет. Но она постепенно появляется. Так, например, я так и не нашел полного описания объекта MODx. Это экземпляр MODext, который потомок ExtJs. Как и не нашел всех типов "xtype". Мало того, некоторые примеры из официальной документации просто не работают. Но это для меня было даже хорошо. Заставило глубже влезть и все-таки запустить все. Чем больше влезаю, тем больше удивление: как все это можно было создать?

Можно создавать свои типы ввода-вывода как положено, через классы. Но можно и проще: тупо создавать нужные ответы на запросы системы. Допустим хотим создать свой тип ввода "Vologda-2"(есть такая у нас станция):

Общий алгоритм такой

  • Нужно создать входной контроллер. Это и есть ответ на запрос системы. При выборе типа, система ищет в определенных каталогах файлы классов PHP или просто файлы PHP.
  • Из контроллера запустить чанк(TPL). Это HTML-шаблон. Хотя можно все и в контроллере сделать. Но не будем отступать от политики MODX отделения логики от данных.

Пусть это будет combo-box со списком поездов. Этот список во внешней таблице. Параметры - дни недели и время прибытия и убытия.

Пример "живой", т.к. на момент написания статьи, сам еще не делал подобного). И если удастся реализовать, значит можно брать заказы на фрилансе). Пока многое не ясно.

Если создать файл "core/model/modx/processors/element/tv/renders/mgr/input/Vologda-2.php" со след. содержимым

<?php
return $modx->controller->fetchTemplate('element/tv/renders/input/Vologda-2.tpl');

И теперь если создать любой новый TV и попытаться выбрать "Тип ввода", то увидим наш новый тип:

Вроде бы мелочь, но приятно))).

Теперь создадим чанк(HTML-шаблон). Сам по себе НТМЛ - для нас бесполезен, он лишь создаст div-контейнер. Главное - это скрипт ExtJS, который в нем запускается. Он и обеспечивает ajax-запросы и всю интерактивность.

Создал таблицу:

CREATE TABLE trails( `id` int not null auto_increment, `name` varchar(255) not null, `rest` varchar(255) null, primary key (`id`) )engine MySAM default charset=utf8 collate=utf8_general_ci
Можно конечно подключить дополнение для пользовательский таблиц. Но сделаем все сами. Ибо если хочешь чтоб все было хорошо - сделай сам). Потом. А пока заполним по старинке. Пока главное с TV разобраться.

insert into trails value(0,'Москва - Воркута','')
insert into trails value(0,'Екатеринбург - Санкт-Петербург','')
insert into trails value(0,'Москва - Лабытнанги','')
insert into trails value(0,'Санкт-Петербург - Астана','')

Ну тут я как рыба в воде). Пока все идет хорошо. Теперь снова в неведомое...

Запуск чанка(TPL) происходит из этого каталога "core/model/modx/processors/element/tv/renders/mgr/inputproperties/". В нем надо создать такой же файл "Vologda-2.php":

<?php
return $modx->controller->fetchTemplate('element/tv/renders/input/Vologda-2.tpl');

Определил опытным путем, разбирая пример. Там об этом подло умолчали.

Сам чанк будет искаться в каталоге "manager/templates/default/" + то, что прописано в контроллере. Т.е. у нас "manager/templates/default/element/tv/renders/input/Vologda-2.tpl". Для отладки создадим этот файл и заполним пока так:

<h3>Привет из Вологды!</h3>

Теперь если обновить страницу с нашим TV и ткнуть в "Тип ввода"... Ну вот). Все крутится как надо. Программисты - счастливые люди. Радуются как дети из-за какой-то ерунды. И вот тут начинается самое жуткое. Суровый ExtJS... Эх, начнем.

Добавим каркас:
<select id="tv{$tv->id}" name="tv{$tv->id}" class="combobox"></select>
<script type="text/javascript">
// <![CDATA[


тут будет тело скрипта


// ]]>
</script>
Это взято из примера(официальная документация)

Немного поясню: CDATA - это чтобы парсер броузера игнорировал HTML-тэги внутри скрипта(на всякий случай, потому как современные броузеры не так глупы) . {...} - это тэги для шаблонизатора Smarty. Все что в них - будет передано ему на обработку. $tv - переменная Smarty, судя по наличию своиств, это объект PHP(наверное строка из базы TV), получаемый Smarty из системы. Хотя как-то странно, по идее, тут все формируется через ajax. Ну ладно. Для начала побалуемся и выведем какое-нибудь свойство активного TV. Это закрепит понимание механизмов.

Поместим в тело:


<select id="tv{$tv->id}" name="tv{$tv->id}" class="combobox"></select>
<script type="text/javascript">
// <![CDATA[
alert({$tv-id});
// ]]>
</script>

И получим... Первый облом). И наверняка далеко не последний. Ладно попробуем вывести весь объект.


<select id="tv{$tv->id}" name="tv{$tv->id}" class="combobox"></select>
<script type="text/javascript">
// <![CDATA[
alert({$tv});
// ]]>
</script>

Обновляем TV, жмем в "Тип ввода"... Это меня озадачило. Порывшись в исходниках, увидел что часть TPL используют '$tv->id', а некоторые '$tv'. У нас передается на самом деле просто ID TV. Но как-то зудело... Какая-то неопределеннось. А это вредно для психики. Начинаешь верить в чудеса. К счастью все разрешилось, когда попробовал запустить саму страницу ресурса с нашим TV. И вот тут все нормально. Тут в Smarty передается объект PHP. И "$tv->id" нормально отрабатывается. Отсюда мораль: проверять типы входа TV надо проверять уже на странице ресурса.

ComboBox в ExtJS - весьма навороченная штука. По умолчанию уже заточен под ajax-загрузку(через JSON) и куча еще впихано. И для передачи обратно на сервер надо обязательно определить "hiddenName". Это во всех компонентах ExtJS. Хотя в некоторых примерах MODX это требование не выполнено и пришлось попотеть, разбираясь почему не пишется TV в базу. Прочтение раздела MODx.combo.ComboBox особо ничего не прояснило. Но зато там есть ссылка на родную документацию ExtJS.

Пока как-то все туманно. Пришлось еще раз перечитать официальную документацию. Все что связано с MODext. Структура каталогов и файлов, реализующих взаимодействие с CMP(страница контента менеджера, если коряво перевести) следующая.

  • manager/assets/modext/ - конфигурации ExtJS, виджеты, панели и прочее
  • manager/controllers/ - PHP файлы, загружающие в менеджер нужные модули ExtJS. Обеспечивают в JS-модулях указатели на коннекторы
  • connectors/ - точка входа для ajax-запросов в менеджере. Тут инициализация MODX, и в конце $modx->getRequest();
  • core/model/modx/processors/ - здесь происходит выборка данных, и необязательно из таблиц. Так же просто для отображения элементов. Например "Тип ввода" для TV.
  • core/model/modx/modmanagerrequest.class.php - диспетчер всех запросов в менеджере
  • core/model/modx/modprocessor.class.php - отвечает за доставку извлеченных данных
Самое забавное, что в родной документации после описания каталогов написано буквально:"Because the MODX manager controllers add an extra layer of complexity and they are not well documented, I'm going to skip them for this demonstration." Типа, поскольку это все сложно и плохо документировано, я пропущу это...". На этом действительно описание этого раздела заканчивается. Замечательно))).

Но, к счастью, есть такой парень Bob Ray, который опубликовал большой список 'xtype'(специализированные для MODx объекты-потомки ExtJS). Верстальщик, конечно, он не очень, но для нас не это гланое. Боб видать давно писал свою статью, в моей версии MODX нужный 'xtype' я нашел тут "manager/assets/modext/widgets/core/modx.combo.js".

Посмотрев как умные люди заполняют комбобоксы, ужаснулся сложности. Сначала создают наследника от xtype:'modx-combo' с помощью заумной функции extend, регистрируют вновь созданный класс в глобальном объекте Ext. И только после этого непосрественно используют. Хотя новый класс практически ничем не отличается от предка. Только значениями свойств. Меня как немного знающего JS, это как-то удивило. Известно что в JS на самом деле нет никакого классического наследования. И все эти штуки больше созданы просто для добавления свойств предка в особый объект на кот. указывает свойство "prototype" , где объект ищет свойства, если не нашел их в себе. К слову сказать есть еще особая область памяти("scope"), где можно также хранить некоторые свойсва через "замыкания". Ну это я так. Эрудицией блеснул). Я решил немного поэкспериментировать. Всял просто xtype:'modx-combo', а в качестве коннектора назначил уже существующий.

<h3>Привет из Вологды!</h3>
<div id="tv{$tv->id}"></div>
<script type="text/javascript">
// <![CDATA[
{literal}
var cb=MODx.load({
{/literal}
xtype: 'modx-combo'
,name: 'tv{$tv->id}'
,hiddenName: 'tv{$tv->id}'
,transform: 'tv{$tv->id}'
,id: 'tv{$tv->id}'
,width: 300
,value: '{$tv->id}'
{literal}
,url: MODx.config.connector_url
,baseParams: {
action: 'element/category/getlist'
,showNone: true
,limit: 0
}
,listeners: { 'select': { fn:MODx.fireResourceFormChange, scope:this}}
});
{/literal}
alert(MODx.config.connector_url);
cb.show();
// ]]>
alert("End!");
</script>

{literal}...{/literal} - это чтоб Smarty не обрабатывал содержание. Это те участки где встречаюся "{" и "}", т.к. эти символы исползует сам Smarty. А алерты я понавставлял, чтоб убедиться, что скрипт обработан до конца и для вывода путей коннектора. Он оказался таким - "/mdx/connectors/index.php", т.е. от корня сайта. А это значит, что можно назначить коннектор и вне MDX. Свойство "transform" - содержит ID HTML-узла, который будет заменен на сгенерированный скриптом контент. Поэтому я его упростил. Ранее был: <select id="tv{$tv->id}" name="tv{$tv->id}" class="combobox"></select>, а стал <div id="tv{$tv->id}"></div>
Только теперь уж проверял на странице ресурса.

И в итоге... Работает! Т.е. ни к чему корячится с новыми классами. Как говорил классик: "Читайте только первоисточники!".

Теперь попробуем подключить свой конектор. Без MODX-совский понтов). Уж больно они навороченные. Оно и понятно, там все через XPDO, а для этого надо загрузить всю махину MODX. Не всегда оправдано напрягать сервер. Из документации ExtJS ясно что коннектор должен отдать JSON. Там приводится протокол, но MODx мог поменять его, поэтому просто глянем что приходит при запуске ComboBox.

Отлично! Вполне вменяемый JSON.

Создадим свой файл коннектора "assets/aset_test/connector_v2.php".

  
 <?php
 include_once($_SERVER['DOCUMENT_ROOT']."/mdx/assets/my_cfg/db.php");
 $db_=new DB_U();  $db_->open();
 $sql_="select * from trails  order by name";
 $r_=mysql_query($sql_); $i=0;
 $data = array(
    'results'=>array(),
    'total' => 0,
    'success'=>true
  );
 while($rw=mysql_fetch_array($r_))
 {
  $data['results'][]=$rw;
  $i++;
 }
 $data['total']=$i;
 print json_encode($data);
Господи, как легко писать на голом PHP и mySQL))).

Теперь разберемся со всеми параметрами комбобокса. В этом поможет только родная английская документация ExtJS.

  • displayField - если отсутствует, то выводится поле "name". При открытии комбобокса
  • id -определяет ID HTML-елемента, если отсутствует-присвается автоматом уникальное
  • name -поле HTML аттрибута name. По умолчанию пустое. Необходимо, если будет включено в форму отправки.(Я этот аттрибут не обнаружил)
  • hiddenName -значение аттрибута name невидимого input, в котором хранится значение элемента для последующей отправки.
  • valueField -имя поле значения. По умолчанию ID.
  • url -путь к коннектору(PHP-handler, по русски))) )
  • listeners - функция-оработчик события. В нашем случае событие - выбор строки.
  • value - Значение для инициализации поля компонента.

После избавления от всего лишнего, наш TPL стал таким:

    
<h3>Привет из Вологды!</h3>
<div id="tv_vologda_fo_repl"></div>
<script type="text/javascript">
// <![CDATA[
{literal}
var cb=MODx.load({
{/literal}
    xtype: 'modx-combo'
    ,hiddenName: 'tv{$tv->id}'
    ,transform: 'tv_vologda_fo_repl'
    ,id: 'tv_vologda'
    ,width: 300
    ,value: '{$tv->value}'
    ,displayField:'name'
    ,valueField:'name'
{literal}
    ,url: '/mdx/assets/aset_test/connector_v2.php'
    ,listeners: { 'select': { fn:MODx.fireResourceFormChange, scope:this}}
});
{/literal}
cb.show();
// ]]>
</script>
ID я переименовал для большей наглядности.

Прочитать сие можно так(пусть $tv->id=6):

  • Загрузить конструктор и создать объект "modx-combo" и установить след. свойства:
  • Создать скрытый input с аттрибутом name="tv6", для установки в нем значения и последующей передаче на сервер(должен быть строго в таком фомате "tv"+id). Я переименовал и перестало писать в базу.
  • Компонент будет установлен вместо HTML-элемента с ID="tv_vologda_fo_repl"
  • Созданный компонент HTML будет иметь id="tv_vologda"
  • Ширина компонента - 300 пикселей
  • При инициализации страницы ресурса в поле нашего TV будет записано имя TV.
  • При выборе значений из комбобокса, строки покажут значения поля "name"
  • Считать именем поля для значений - "name". Т.е. при выборе строки в hidden input будет помещено значение поля "name". И соответственно, передано на сервер при сохранении.
  • В качестве ajax handler использовать '/mdx/assets/aset_test/connector_v2.php'
  • После выбора строки запустить функцию MODx.fireResourceFormChange. Fierer обработчика - сам компонент(JS-объект).
  • Сделать компонент видимым.(Ну это на всякий случай, он вроде сразу видим.)

Все работает!

И в базу TV все пишется!

Теперь надо куда-то впихнуть время прибытия и отправления. Можно конечно создать еще TV, но такой подход чреват огромным кол-вом TV, что в итоге приведет к хаосу. Из всего выше проделанного вытекает мысль, что можно вывести что угодно в TV, в том числе сколько угодно полей ввода. К тому же при просмотре таблицы со значениями TV, я обнаружил, что некоторые TV хранят не просто текст, а явно что-то JSON - подобное. Еще раз перечитав все о TV в родной документации, ничего полезного там не обнаружил. Придется смотреть исходники. Например galleryitem. У него очень навороченный тип ввода. Документация по Gallery тоже не раскрыла нужной мне инфы. Там больше про применение.

Пережив очередной шок от увиденного, нашел таки начало цепи вызовов. А начало идет от плагина GalleryCustomTV. Это несколько иной путь создания типов ввода TV. Плагин только устанавливает пути для прорисовки типов ввода и вывода TV. Такие плагины так и называются - Pathing Plugin. Устанавливает пути до входных и выходных контроллеров(кусков PHP).

    OnTVInputRenderList - представление TV на странице ресурса(естественно в бэкенде)
    OnTVOutputRenderList - то же, но для фронт-енда. 
    OnTVInputPropertiesList - отображает параметры TV на странице самого TV в бэкенде
    OnTVOutputRenderPropertiesList - то же, но для фронт-енда. 
    OnDocFormPrerender - загружает, если надо, собственные JS/CSS для TV

Для gallery устанавливает следующие пути

    OnTVInputRenderList - "core/components/gallery/elements/tv/input/" (там нам нужен - galleryitem.php)
    OnTVOutputRenderList - "core/components/gallery/elements/tv/output/"  (galleryitem.php)
    OnTVInputPropertiesList - "core/components/gallery/elements/tv/inputoptions/" (тут для нас ничего нет)
    OnTVOutputRenderPropertiesList - "core/components/gallery/elements/tv/properties/"   (galleryitem.php)
    OnDocFormPrerender - "assets/components/gallery/js/mgr". Загрузка js и css отсюда и из подкаталогов
    OnManagerPageBeforeRender - грузятся 'gallery.js', 'tree.js', в GAL.config.connector_url устанавливает путь к коннектору("assets/components/gallery/connector.php").

Итак сначала вызывается "core/components/gallery/elements/tv/input/galleryitem.php"

Судя по коду, вызывается в контексте объекта. Т.к. в this->value - значение TV. Далее через $modx->fromJSON($this->value) преобразуется в массив. Преообразуется в другой массив, снова в JSON, который назначается переменной Smarty 'itemjson'
И возвращает результат работы Smarty с "core/components/gallery/elements/tv/galleryitem.input.tpl"

core/components/gallery/elements/tv/galleryitem.input.tpl

Создается Контейнер "tv{$tv->id}-form" для компонента, hidden input c name="tv{$tv->id}" и таким же ID. В value этого input Smarty впихивает ранее назначенную переменную 'itemjson'. Если это ему не удается, то пустой JSON("{}"). Надо полагать и отправляет с него же.
Загружает xtype: 'gal-panel-tv' c параметрами tv=tv->id,tvValue=tv->value, data=itemjson.
Пока все понятно). Ну кроме момента с переделкой JSON из одного в другой. Может потом прояснится.

xtype: 'gal-panel-tv'

Нашел я его тут "assets/components/gallery/js/mgr/tv/galtv.js".
Весь JS крутится вокруг глобального объекта GAL(зарегистрирован под именем gallery).

  • Перекрывается метод "onDrag" объекта Ext.slider.Thumb. Мне пока это по-боку...
  • Объявляется конструктор GAL.TV, кот. и регистрируется как 'gal-panel-tv'. Ну вот осталось последний бастион взять и все прояснится).
  • Это потомок MODx.Panel. Т.е. и сам - просто панель, содержащий набор компонентов(поля ввода, чек-боксы и прочее)
  • Объект огромный, но нам бы выбрать все что касается установки компонентов, загрузки в них данных и выгрузки на сервер.

GAL.TV

  • this.previewTpl - шаблон для просмотра изображения. Оптимизация шаблона через this.previewTpl.compile().
  • var item = this.previewTpl.applyTemplate(config.data); - создается локальная переменная item с полями, заполненными нашим JSON. В ExtJS есть свой шаблонизатор. И тэги у него как в Smarty. Что меня сначало запутало. Но это все касается отображения самого изображения. Меня же интересует прорисовка компонент.
  • с помощью Ext.applyIf config дополняются дополняется недостающими поля.
    • размеры, рамки и прочая требуха
    • renderTo: 'tv'+config.tv+'-form' - HTML-элемент(его ID) где будет размещена наша панель
    • ,items: - вот то что нужно! Это и есть массив компонентов. Наверное...
      • а вот и само скрытое поле для хранения ID, а скрытое, т.к. не нужно его видеть менеджеру.
                        xtype: 'hidden'
                        ,name: 'gal_id'
                        ,id: 'tv'+config.tv+'-gal_id'
                        ,value: config.data['gal_id'] || 0
                          
      • вот текстовое поле для ввода, нужное мне
                        xtype: 'textfield'
                        ,name: 'gal_image_height'
                        ,id: 'tv'+config.tv+'-gal_image_height'
                        ,fieldLabel: _('gallery.height')
                        ,value: config.data['gal_image_height'] || 0
                        ,anchor: '97%'
                        ,listeners: {'change':this.syncHidden,scope:this}
                          
      • обработчик вызываемый при изменении текстового поля
                syncHidden: function(tf,nv,nm) 
                    {
                        var v = tf.getValue();
                        if (typeof v != 'number' && typeof v != 'boolean' && typeof v != 'object') 
                            {
                                v = v.replace("'",''');
                            }
                        else if (typeof v == 'object')
                            {
                                v = v.value;
                            }
                        var n = tf.getName ? tf.getName() : nm;
                        this.setValue(n,v+'');
                        this.updateImage();
                    }
         /*
         - если текст, то знаки "'" заменяются на HTML-сущность (бесит меня этот термин)
         - tf->это объект xtype вызвавший обработчик(назначается в свойстве "scope")
         - считывается значение аттрибута "name"
         - и по этому имени пишется значение нашего текстового поля в JSON
         - теперь ясно, что сам обработчик выполняется в контексте GAL.TV
         - и служит для обновления отображения компонентов при смене значений полей
         */
                          
      • GAL_TV.updateImage

            updateImage: function() 
            {
                var vs = this.getValues();
                var p = Ext.get('tv'+this.config.tv+'-preview');
                if (p) 
                {
                    this.previewTpl.overwrite(p,vs);
                }
                this.imageBox = Ext.get('tv'+this.config.tv+'-image').getBox();
                if (this.resizer) {this.resizer.imgBox = this.imageBox;}
                return true;
            }
         /*
          - vs - именованный массив данных, выбранный методом getValues()
          - Ext.get - возвращает элемент Ext, в отличие от Ext.getCmp, кот. вернет компонент Ext (а он,надо полагать, круче элемента))) пока не будем этим грузится)
          - короче в конце тут просто перерисовка изображения
         */
                          
      • GAL_TV.getValues

            getValues: function() 
            {
                var f,i,v;
                var vs = {};
                for (i=0;i<this.fields.length;i++) 
                {
                    vs[this.fields[i]] = this.getValue(this.fields[i]);
                }
                return vs;
            }
         /*
          - в GAL.TV есть массив имен(обычный, неименованный) типа ['gal_id','gal_album',...
          - а наша функция создает именованный массив, имена берет из массива имен, а значения уже из формы по аттрибутам "name". наверное...
          - ан нет. оказалось input ищется по ID со значением 'tv'+this.config.tv+'-'+var_name.
          - т.е. если у нас tv.id=6 и имя_поля="okey", то будем искать HTML-элемент с ID="tv6-okey"
          - переменные f и v тут явно лишние)
         */
                          
      • GAL_TV.setValue

            setValue: function(k,v) 
            {
                var f = this.findField(k);
                if (f && f.setValue) 
                {
                    var vs = {};
                    vs[k] = v;
                    this.setHiddenField(vs);
                    return f.setValue(v);
                }
                return false;
            }
         /*
            - находит Ext-компонент c id=("tv"+TV.id+"-"+k) напр.:"tv6-okey"
            - т.к. в js нет именованный массивов (в отличие от PHP), в качестве оных используются объекты.
            - так вот, создается объект vs, в него добавляется свойство k, которому присвоивается значение v.
            - и этот новый объект(с ключом и значением) передается в setHiddenField(напр.:{okey:yes})(чувствуется развязка...)
            - в конце найденному компоненту присваиваем его же значение. это забавно конечно.... просто, видно, функция универсальная
         */
                          
      • GAL_TV.findField

            findField: function(k) 
            {
                var f = Ext.getCmp('tv'+this.config.tv+'-'+k);
                return f ? f : null;
            }
         /*
            - возвращает Ext-компонент(не элемент!), у которого ID="tv6-okey"(если ID нашего TV = 6, а имя поля в json ="okey")  
         */
                          
      • GAL_TV.setHiddenField

            setHiddenField: function(data) 
            {
                var fld = Ext.get('tv'+this.config.tv);
                var js = Ext.decode(fld.dom.value);
                js = Ext.apply(js,data);
                fld.dom.value = Ext.encode(js);
                Ext.getCmp('modx-panel-resource').markDirty();
            }                      
         /*
            - на входе имеем объект с ключом и значением(напр.:{okey:yes}) 
            - fld - это наш скрытый input, в котором сидит весь исходный JSON в виде строки(т.е. литеральный json), ID="tv6"
            - Ext.decode - делает из литерального JSON(из нашего hidden input) объект js.
            - впихивает полученный на входе json({okey:yes}) в большой(извлеченный из input)
            - и исправленный большой JSON опять помещает в наш hidden input (ID="tv6")
            - помечает всю панель ресурса как "грязную", типа были изменения
            - уффф. вот и все! цепь замкнулась.
         */
                          

Подведем итог разбора исходников galleryitems

Алгоритм работы со сложным TV

  • Все поля хранятся в литеральном JSON
  • При загрузке TV надо создать скрытый input c аттрибутом name="tv"+TV.ID(напр.:"tv6"). И в него поместить весь JSON из TV.
  • При прорисовке всех input нашего TV, их значения заполняются соответствующими полями из JSON. Тут уж полная свобода.
  • При изменении в input's надо обеспечить:
    • Обновление литерального JSON в скрытом input(ID="tv6")
    • Пометить панель ресурсов как "грязную"(т.е. признак необходимости сохранения)
  • Ну и при сохранении ресурса наш input(ID="tv6") отправит свой JSON на сервер(в переменной POST['tv6']), а тот запишет его как $tv->value. Эх! Как на танке по Берлину)

Вот теперь ясно, что придется переделать почти все). Зато уже многое прояснилось. Ну а освоив собственные составные TV, можно уже угомонится и свободнее ваять сложные сайты.

Сам life-процесс создания составного TV я решил описать в отдельном посте.


продолжение следует.....



Некоторые термины:

  • Processors - файлы PHP модифицирующие базу данных
  • Connectors - "мосты" к процессорам. Управляют доступом. Обеспечивают соединение на уровне "модели"(model layer)
  • Controllers - файлы PHP отвечающие за представление CMP(страницы пользователя в менеджере)
  • Пространства имен - для каждого дополнения(или пользовательской страницы в менеджере) содержит пути к ядру(core) и к активам(assets)
  • Действия(acts) - содержат,в частности, пути к контроллерам, запускаемым при выборе пользователького пункта меню(запускается пользователькая страница)
  • PDO - PHP Data Object
  • ORB - Object Relational Bridge

Некоторые ссылки:



продолжение следует.....

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

** - в последнее время прослеживается обратная тенденция. Пример - файлы конфигурации в Wayfinder, Formit и прочих. Где конфигурация - внешний php-код с параметрами и прямо там прописанными шаблонами вывода. Связано это с дрейфом верстки от дизайнеров в сторону программистов. А последним намого удобнее все делать в одном файле, чем скакать по куче чанков, сниппетов и прочих блоков.


Комментарии 0






Разрешённые теги: <b><i><br>Добавить новый комментарий: