Pull to refresh

Разработка дополнения для MODx Revolution. Часть 1

Reading time11 min
Views35K

На 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. По третьей части, по-моему ещё никто не писал, возможно скоро сделаю перевод. Там рассказывается об упаковке всего компонента в пакет, который можно будет легко установить через раздел «Управление пакетами» в системе управления.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+6
Comments3

Articles

Change theme settings