
На habrahabr уже много писали о создании компонентов для MODx Revolution, но по моему мнению исчерпывающего руководства на русском языке до сих пор нет. Очень много пробелов и не совсем точной информации. Но такое руководство есть в официальной документации на английском. Я думаю в таком деле самодеятельность ни к чему :). Хотя у настоящего программиста не должно быть проблем с английским, читать по-русски думаю для большинства приятнее. Я решил сделал вольный перевод этой фундаментальной, на мой взгляд, части документации. Предлагаю вашему вниманию первую часть. Надеюсь хватит терпения закончить эту работу, а возможно кто-то мне в этом поможет. Не пугайтесь, что много текста, это кажется сложным только на первый взгляд.
Обзор
В этом уроке будет рассказано о разработке простого дополнения «Doodles» (болванки), использующем пользовательскую таблицу базы данных для хранения объектов, называемых «Doodles», которые имеют имя и описание. Мы создадим сниппет, который будет выводить список, оформленный по шаблону через чанк, свою страницу администрирования (компонент) с использованием ExtJS (часть 2), а также сделаем скрипт для упаковки в пакет (package) — часть 3. Также всё это будет i18n-совместимым, т.е. иметь файлы для перевода на разные языки. Кстати, этот пакет можно скачать и хорошенько изучить.
Создание структуры каталогов
Итак, я создал на своём локальном сервере папку /www/doodles/. Эта папка у меня доступна по адресу http ://localhost/doodles/. Наша структура каталогов выглядит так:
Отметим несколько вещей. Во-первых, мы имеем 3 главных каталога: core/, assets/ и _build/. Обычно дополнения MODx Revo разделены на два каталога: core/components/myextra/ и assets/components/myextra/. Каталог assets/components/ содержит только веб-специфические файлы — JavaScript, CSS и т.д. Это файлы, которые публично доступны в Интернете. Все PHP-файлы будут находиться в каталоге core/components/. Каталог core/ может быть перемещен за пределы корневой директории сайта (webroot) для дополнительной безопасности. Эти каталоги будут упакованы в транспортный пакет нашего компонента для возможности быстрой установки из раздела «Управление пакетами» административной части сайта. Каталог _build/ не будет упакован в транспортный пакет. Он нужен только на стадии создания этого пакета. Об этом будет рассказано в последней части урока.В папке assets/ у нас будет только один PHP-файл — connector.php. Этот файл позволит нам иметь доступ к процессорам нашей пользовательской страницы управления (Custom Manager Page — CMP). Об этом позже.
В директории core/components/doodles/ создадим несколько каталогов:
controllers — контроллеры для CMP;
docs — содержит только файлы changelog, readme и лицензии;
elements — все наши сниппеты, плагины, чанки и т.д.;
lexicon — все i18n языковые файлы;
model — наши классы, а также XML-файл схемы для наших пользовательских таблиц базы данных;
processors — все наши процессоры для CMP.
Создание сниппета
Создадим файл сниппета: /www/doodles/core/components/doodles/elements/snippets/snippet.doodles.php. Добавим в файл несколько строчек кода:
$doodles = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties); if (!($doodles instanceof Doodles)) return '';
Ой! Что это такое? Это то место, где происходит волшебство. Давайте разберем каждую часть. Во-первых, у нас есть вызов метода getService(). Разобьем эту строку на несколько частей для более удобного чтения:
$defaultDoodlesCorePath = $modx->getOption('core_path').'components/doodles/'; $doodlesCorePath = $modx->getOption('doodles.core_path',null,$defaultDoodlesCorePath); $doodles = $modx->getService('doodles','Doodles',$doodlesCorePath.'model/doodles/',$scriptProperties);
Хорошо, что такое $modx->getOption()? С помощью этого метода можно узнать наши системные настройки. $modx->getService() загружает класс, создает экземпляр объекта и устанавливает его в $modx->doodles, в этом случае (первый параметр, передаваемый в метод). Подробнее здесь.
Создание настроек путей
В системе управления перейдем «Система» -> «Настройки системы». Создадим два новых параметра с соответствующими значениями:
doodles.core_path — {core_path}components/doodles/
doodles.assets_url — {assets_path}components/doodles/
Создание базового класса
Создадим файл класса /www/doodles/core/components/doodles/model/doodles/doodles.class.php. Этот класс нам будет полезен тем, что в нем мы можем определить некоторые основные пути, а также методы, которые будем использовать в нашем компоненте.
<?php class Doodles { public $modx; public $config = array(); function __construct(modX &$modx,array $config = array()) { $this->modx =& $modx; $basePath = $this->modx->getOption('doodles.core_path',$config,$this->modx->getOption('core_path').'components/doodles/'); $assetsUrl = $this->modx->getOption('doodles.assets_url',$config,$this->modx->getOption('assets_url').'components/doodles/'); $this->config = array_merge(array( 'basePath' => $basePath, 'corePath' => $basePath, 'modelPath' => $basePath.'model/', 'processorsPath' => $basePath.'processors/', 'chunksPath' => $basePath.'elements/chunks/', 'jsUrl' => $assetsUrl.'js/', 'cssUrl' => $assetsUrl.'css/', 'assetsUrl' => $assetsUrl, 'connectorUrl' => $assetsUrl.'connector.php', ),$config); } }
Теперь вернемся к нашему сниппету. Добавим несколько свойств:
$dood = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties); if (!($dood instanceof Doodles)) return ''; /* setup default properties */ $tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl'); $sort = $modx->getOption('sort',$scriptProperties,'name'); $dir = $modx->getOption('dir',$scriptProperties,'ASC'); $output = ''; return $output;
Круто. Теперь мы хотим использовать xPDO для запросов к базе данных, чтобы захватывать наши записи… ой. Мы еще не сделали xPDO модель для них. Мы должны сделать это.
Создание модели
xPDO делает абстракцию баз данных в симпатичные методы запросов ООП. В настоящее время он начинает поддерживать несколько баз данных. Кроме того, он позволяет превратить ваши строки БД в хорошие, чистые классы и сделать всё это очень короткими строками кода. Но сначала мы должны построить эту модель, используя схемы xPDO.
Создайте XML файл /www/doodles/core/components/doodles/model/schema/doodles.mysql.schema.xml. Поместите в него это:
<?xml version="1.0" encoding="UTF-8"?> <model package="doodles" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM"> <object class="Doodle" table="doodles" extends="xPDOSimpleObject"> <field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/> <field key="description" dbtype="text" phptype="string" null="false" default=""/> <field key="createdon" dbtype="datetime" phptype="datetime" null="true"/> <field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" /> <field key="editedon" dbtype="datetime" phptype="datetime" null="true"/> <field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" /> <aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/> <aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/> </object> </model>
Разберемся что всё это значит. Первая строка:
<model package="doodles" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM">
содержит название нашего пакета — «doodles». Она также говорит, что нашим базовым классом будет«xPDOObject», и что эта схема сделана для MySQL. Также она говорит, что нашей системой хранения данных MySQL по умолчанию будет MyISAM. Дальше задаются свойства таблицы базы данных. Не нужно создавать поле с идентификатором, т.к. его всегда создает объект xPDOSimpleObject и устанавливает auto-increment.
<aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/> <aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
Тут создается связь с другими объектами xPDO (в данном случае с объектом пользователей).
Скрипт парсинга схемы
Теперь пришло время обратить внимание на наш каталог _build/. Создадим в нем файл /www/doodles/_build/build.config.php с таким содержанием:
<?php define('MODX_BASE_PATH', '../'); define('MODX_CORE_PATH', MODX_BASE_PATH . 'core/'); define('MODX_MANAGER_PATH', MODX_BASE_PATH . 'manager/'); define('MODX_CONNECTORS_PATH', MODX_BASE_PATH . 'connectors/'); define('MODX_ASSETS_PATH', MODX_BASE_PATH . 'assets/'); define('MODX_BASE_URL','/modx/'); define('MODX_CORE_URL', MODX_BASE_URL . 'core/'); define('MODX_MANAGER_URL', MODX_BASE_URL . 'manager/'); define('MODX_CONNECTORS_URL', MODX_BASE_URL . 'connectors/'); define('MODX_ASSETS_URL', MODX_BASE_URL . 'assets/');
Проверьте правильность всех путей для вашего случая. Теперь создадим сам скрипт, который проанализирует нашу XML-схему и создаст её PHP-представление. Создаем файл /www/doodles/_build/build.schema.php:
<?php require_once dirname(__FILE__).'/build.config.php'; include_once MODX_CORE_PATH . 'model/modx/modx.class.php'; $modx= new modX(); $modx->initialize('mgr'); $modx->loadClass('transport.modPackageBuilder','',false, true); echo '<pre>'; /* used for nice formatting of log messages */ $modx->setLogLevel(modX::LOG_LEVEL_INFO); $modx->setLogTarget('ECHO'); $root = dirname(dirname(__FILE__)).'/'; $sources = array( 'model' => $root.'core/components/doodles/model/', 'schema_file' => $root.'core/components/doodles/model/schema/doodles.mysql.schema.xml', ); $manager= $modx->getManager(); $generator= $manager->getGenerator(); if (!is_dir($sources['model'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Model directory not found!'); die(); } if (!file_exists($sources['schema_file'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Schema file not found!'); die(); } $generator->parseSchema($sources['schema_file'],$sources['model']); echo 'Готово.'; exit();
Теперь вы можете запустить файл _build/build.schema.php. Я делаю это путем загрузки в веб-браузере: http ://localhost/doodles/_build/build.schema.php. После этого будут сгенерированы файлы классов и карты.

Теперь давайте сделаем небольшую корректировку нашего базового класса Doodles (/www/doodles/core/components/doodles/model/doodles/doodles.class.php). Добавим в конструктор класса сразу после array_merge такую строку:
$this->modx->addPackage('doodles',$this->config['modelPath']);
Это говорит xPDO, что мы хотим добавить xPDO пакет «doodles», что позволит нам делать запросы к нашей пользовательской таблице.
Сниппет include
Ранее мы создали сниппет. Чтобы им воспользоваться можно теперь создать сниппет в системе управления и вставить код из файла doodles/elements/snippets/snippet.doodles.php. Но код такого сниппета может быть не удобно редактировать. Чтобы это исправить можно создать простой универсальный сниппет «include». Перейдем в системе управления «Элементы» -> «Сниппеты» -> «Новый сниппет».
Имя сниппета:
include
Код сниппета (php):
<?php return include $file;
Теперь на любой странице или в любом шаблоне можно сделать такой вызов сниппета:
[[!include? &file=`[[++doodles.core_path]]elements/snippets/snippet.doodles.php`]]
Создание запросов к БД
Во-первых нам необходимо создать таблицу. Для этого просто добавим в наш сниппет до возвращения значения (return) такие строки:
$m = $modx->getManager(); $created = $m->createObjectContainer('Doodle'); return $created ? 'Таблица создана.' : 'Таблица не создана.';
Теперь, если запустить наш сниппет, в базе данных автоматически будет создана таблица компонента «Doodles». После этого можно удалить этот код, а лучше сделаем так:
$tablexists = $modx->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '".$modx->getOption('dbname')."' AND table_name = '".$modx->getOption('table_prefix')."doodles'"); if(!$tablexists->fetch(PDO::FETCH_COLUMN)){ $m = $modx->getManager(); $created = $m->createObjectContainer('Doodle'); }
Теперь добавим в сниппет такие строки:
$doodles = $modx->getCollection('Doodle'); $output = count($doodles);
На выходе вы должны увидеть «0», т.к. таблица ещё пуста.
Создайте в таблице пару строк (например при помощи phpMyAdmin) и вы увидите как сниппет выводит их число. Также строки в таблице можно создать так:
$doodle = $modx->newObject('Doodle'); $doodle->fromArray(array( 'name' => 'TestDoodle', 'description' => 'A test doodle' )); $doodle->save();
Не забудьте удалить этот код.
Отлично! Пользовательский запрос к базе данных работает! Давайте сделаем его более сложным. Мы можем использовать xPDOQuery xPDO, чтобы создать несколько довольно сложных запросов. А сейчас давайте просто добавим команды сортировки:
$c = $modx->newQuery('Doodle'); $c->sortby($sort,$dir); $doodles = $modx->getCollection('Doodle',$c);
Сортировка происходит по полю $sort в порядке $dir. Значение этих переменных мы определили выше. В вызове сниппета это будет выглядеть так:
[[!include? &file=`[[++doodles.core_path]]elements/snippets/snippet.doodles.php`&sort=`name`&dir=`DESC`]]
Метод getChunk класса Doodles
Добавим в базовый класс пару вспомогательных методов:
public function getChunk($name,$properties = array()) { $chunk = null; if (!isset($this->chunks[$name])) { $chunk = $this->_getTplChunk($name); if (empty($chunk)) { $chunk = $this->modx->getObject('modChunk',array('name' => $name)); if ($chunk == false) return false; } $this->chunks[$name] = $chunk->getContent(); } else { $o = $this->chunks[$name]; $chunk = $this->modx->newObject('modChunk'); $chunk->setContent($o); } $chunk->setCacheable(false); return $chunk->process($properties); } private function _getTplChunk($name,$postfix = '.chunk.tpl') { $chunk = false; $f = $this->config['chunksPath'].strtolower($name).$postfix; if (file_exists($f)) { $o = file_get_contents($f); $chunk = $this->modx->newObject('modChunk'); $chunk->set('name',$name); $chunk->setContent($o); } return $chunk; }
Пока, все, что вам нужно знать, что эти методы будут искать чанки в вашем каталоге /www/doodles/core/components/doodles/elements/chunks/.
Создадим файл чанка /www/doodles/core/components/doodles/elements/chunks/rowtpl.chunk.tpl с примерно таким содержинием:
<li><strong>[[+name]]</strong> - [[+description]]</li>
В наш сниппет добавим такие строки:
foreach ($doodles as $doodle) { $doodleArray = $doodle->toArray(); $output .= $dood->getChunk($tpl,$doodleArray); }
Полный код сниппета получился такой:
<?php $dood = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties); if (!($dood instanceof Doodles)) return ''; /* setup default properties */ $tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl'); $sort = $modx->getOption('sort',$scriptProperties,'name'); $dir = $modx->getOption('dir',$scriptProperties,'ASC'); $output = ''; $tablexists = $modx->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '".$modx->getOption('dbname')."' AND table_name = '".$modx->getOption('table_prefix')."doodles'"); if(!$tablexists->fetch(PDO::FETCH_COLUMN)){ $m = $modx->getManager(); $created = $m->createObjectContainer('Doodle'); } $c = $modx->newQuery('Doodle'); $c->sortby($sort,$dir); $doodles = $modx->getCollection('Doodle',$c); foreach ($doodles as $doodle) { $doodleArray = $doodle->toArray(); $output .= $dood->getChunk($tpl,$doodleArray); } return $output;
На выходе увидим список наших тестовых данных из таблицы:

Итак, мы загрузили базовый класс по путям, созданным в настройках системы, достали xPDO пакет из пользовательской таблицы базы данных и вывели его по шаблонку с помощью чанка.
В следующей части этого урока рассказывается о создании административной страницы (компонента). Об этом на хабре недавно неплохо рассказал bezumkin. По третьей части, по-моему ещё никто не писал, возможно скоро сделаю перевод. Там рассказывается об упаковке всего компонента в пакет, который можно будет легко установить через раздел «Управление пакетами» в системе управления.
