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


    На 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. По третьей части, по-моему ещё никто не писал, возможно скоро сделаю перевод. Там рассказывается об упаковке всего компонента в пакет, который можно будет легко установить через раздел «Управление пакетами» в системе управления.
    Метки:
    • +6
    • 26,2k
    • 3
    Поделиться публикацией
    Комментарии 3
    • +1
      С нетерпением жду следующих статей! Сейчас стоит вопрос о выборе CMS для одного сайта между Drupal и MODx. Из-за незнания некоторых тонких моментов не могу пока прийти к MODx, но система нравится. Один из этих моментов — как раз создание дополнений.
      • 0
        Здорово! Это очень полезная статья!
        А есть уже 2-я часть? Если есть, можно ссылку на неё?

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