На 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. По третьей части, по-моему ещё никто не писал, возможно скоро сделаю перевод. Там рассказывается об упаковке всего компонента в пакет, который можно будет легко установить через раздел «Управление пакетами» в системе управления.