MODX. Схема работы компонентов / дополнений MODX

Среди множества статей по созданию компонентов MODX, многие устарели, а некоторые привязаны к необязательным знаниям, таким как: работа с удаленными репозиториями, или в конкретных необязательных программах.

Тут я постараюсь собрать схемы, по которым работают компоненты в текущей версии MODX (на момент написания — версия 2.7) с помощью которых можно научиться понимать их структуру, и писать свои, без привязки к чужим статьям. Если увидели ошибку, или есть какой-либо иной комментарий по схеме работы компонента, пожалуйста, отправьте свое виденье через форму обратной связи.

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

Из чего состоит компонент? Компонент это набор файлов с определенной логикой. В него могут входить js, css, php и другие файлы, скомпанованные в определенную структуру, и вот пример организации этой структуры (количество файлов и папок может меняться в зависимости от необходимости наличия в компоненте, но структуру и стиль наименования стоит соблюдать, для нормальной работы и последующей понимания другими разработчиками)

Базовая структура компонентов MODX Revolution

  • assets — файлы, которые должны быть доступны снаружи, из интернета.
    • components — каталог в котором хранятся файлы компонентов, которые должны быть доступны из браузера.
      • имя_компонента
        • connector.php — файл обеспечивающий связь между внешними файлами и теми что обрабатываются на сервере.
        • файлы и каталоги компонента — чаще всего встречаются: каталоги js, css, img. Если компонент содержит в себе панель управления менеджера, то в структуру добавится следующая конструкция:
          • js — каталог содержащий js файлы.
            • mgr — каталог содержащий файлы связанные с панелью управления менеджера.
              • имя_компонента.js — файл, в котором инициализируется главный объект страниц менеджера компонента.
              • sections — каталог, в котором лежат файлы, связанные со страницами компонента в панели менеджера.
                • home.js — файл скрипта который рендерит главную страницу компонента.
              • widgets — каталог, в котором лежат файлы, связанные с виджетами, которые подключаются на страницах панели менеджера компонента. Чьобы узнать, какие основные стандартные ExtJS-объекты есть в MODX, можно посмотреть в папку /manager/assets/modext/widgets/
                • home.panel.js — файл скрипта виджета «Панель», который содержит в себе заголовок, описание и табы (вкладки).
                • _____.grid.js — файл скрипта виджета работающего с таблицой элементов modExtra. Основной рабочий файл, который выводит записи из БД и позволяет редактировать их во всплывающих окошках.
  • core — файлы, составляющие ядро MODX.
    • components — каталог в котором хранятся файлы компонентов, которые должны быть скрыты для доступа из браузера.
      • имя_компонента
        • файлы и каталоги компонента — есть определенный устоявшийся набор каталогов компонента:
          • controllers — файлы для подготовки страниц админки (контроллеры). Загружают нужные скрипты и стили.
          • docs — история изменений, инструкция и лицензия. Эти файлы участвуют в описании пакета с компонентом.
          • elements — устанавливаемые чанки, сниппеты и прочие возможные наследники modElement.
          • lexicon — словари компонента, обычно только en и ru. Подробнее о словарях.
          • model — директория с объектами компонента и моделями таблиц для баз данных, обычно только для MySql. Здесь же находится и основной рабочий класс компонента.
            • schema — тут хранятся XML схемы компонента, формирующие связи с БД.
              • имя_компонента.mysql.schema.xml —​​​​ XML схема компонента для связи с БД MySql.
              • имя_компонента.mssql.schema.xml —​​​​ XML схема компонента для связи с БД MsSql (другие делаются по аналогии).
            • имя_компонента
              • mysql — каталог, в котором хранятся файлы обеспечивающие связь с базой MySql (чтобы добавить связи с другими типами баз, надо создать соответствующие каталоги с аналогичными файлами). При создании компонента с помощью ModExtra, файлы внутри этого каталога перезаписываются при каждой генерации модели по новой схеме.
                • имя_объекта.class.php — расширение объекта класса, для связи с БД MySql. 
                • имя_объекта.map.inc.php — карта объекта, которая используется для MySql. В ней прописываются все поля, индексы и связи, которые задаются в схеме XML, которая находится по адресу model/schema/имя_компонента.mysql.schema.xml
              • metadata.mysql.php — общая информация о том, какие обхекты есть в компоненте. Этот представление используется для xPDO, создаётся один общий объект, который расширяется при работе в определённой системе.
              • имя_объекта.class.php — объекты определенного класса компонента, и все его основные методы.
          • processors — файлы, выполняющие какую-то одну небольшую пользовательскую функцию. Служат, как правило, для обработки запросов от админки.
          • index.class.php — сюда выносится адрес коннектора, особенно если в дополнении используется множество js файлов. 
  • manager — иногда файлы относящиеся к админ панели /manager/ размещают здесь.
    • components — каталог в котором могут храниться файлы компонентов, которые должны быть доступны в админ панели.
      • имя_компонента
        • файлы и каталоги компонента — все как в предыдущих примерах...

Пространство имен

Любой компонент начинается с создания «пространства имен» (namespaces). Оно создается в системном меню, в одноименном пункте.

В статье будет рассмотрен компонент с названием Sample. Итак:

  1. Нажимаем кнопку «создать новое»;
  2. В строке Core Path прописываем путь к ядру: {core_path}components/sample/
  3. В строке Assets Path прописываем путь к активам: {assets_path}components/sample/

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

Файл connector.php

Файл assets/components/sample/connector.php служит мостом для объединения логики внешних и внутренних файлов, из открытого каталога assets и закрытого каталога core. Пример стандартного файла коннектора:

// Добавляем если надо отловить ошибки Ajax
ini_set('display_errors', 1);
ini_set('error_reporting', -1);

// Подключаем MODX
require_once dirname(dirname(dirname(dirname(__FILE__)))).'/config.core.php';
require_once MODX_CORE_PATH.'config/'.MODX_CONFIG_KEY.'.inc.php';
require_once MODX_CONNECTORS_PATH.'index.php';

// Получаем настройки, подключаем и инициализируем класс
$corePath = $modx->getOption('sample.core_path',null,$modx->getOption('core_path').'components/sample/');
require_once $corePath.'/model/sample/sample.class.php';
$modx->sample = new sample($modx);

// Получаем лексические значения
$modx->lexicon->load('sample:default');

// Указываем путь к процессорам и запускаем обработчик 
$path = $modx->getOption('processorsPath',$modx->sample->config,$corePath.'processors/');
$modx->request->handleRequest(array(
    'processors_path' => $path,
    'location' => '',
));

Если нужна возможность подгружать конфиги из каталога в корне, заменяем код подключения: 

if (file_exists(dirname(dirname(dirname(dirname(__FILE__)))) . '/config.core.php')) {
	require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/config.core.php';
}
else {
	require_once dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/config.core.php';
}

Методы подключения файлов с классами разнятся от разработчика к разработчику, и во многом зависят от личных предпочтений. Узнать чуть больше об их различиях можно в статье посвященной методам loadClass(), getService(), addPackage() и addExtensionPackage().

Основной класс компонента

Основной класс компонента находится по адресу: core/components/sample/model/sample/sample.class.php

В нем содержатся методы необходимые компоненту, в каждом случае свои, но, как правило, присутствует метод __construct(), в котором задается переменная Sample::config. Доступ к переменной $sample->config.

Типовой набор для метода __construct():

private $modx; // Объявляем переменную, которая будет содержать ссылку на объект modx

function __construct(modX &$modx, array $config = array()) {
		$this->modx =& $modx;
		$corePath = $this->modx->getOption('sample_core_path', $config, $this->modx->getOption('core_path') . 'components/sample/');
		$assetsUrl = $this->modx->getOption('sample_assets_url', $config, $this->modx->getOption('assets_url') . 'components/sample/');
		$connectorUrl = $assetsUrl . 'connector.php';
		$this->config = array_merge(array(
			'assetsUrl' => $assetsUrl,
			'cssUrl' => $assetsUrl . 'css/',
			'jsUrl' => $assetsUrl . 'js/',
			'imagesUrl' => $assetsUrl . 'images/',
			'connectorUrl' => $connectorUrl,
			'corePath' => $corePath,
			'modelPath' => $corePath . 'model/',
			'chunksPath' => $corePath . 'elements/chunks/',
			'templatesPath' => $corePath . 'elements/templates/',
			'chunkSuffix' => '.chunk.tpl',
			'snippetsPath' => $corePath . 'elements/snippets/',
			'processorsPath' => $corePath . 'processors/'
		), $config);
		$this->modx->addPackage('sample', $this->config['modelPath']);
		$this->modx->lexicon->load('sample:default');
	}

Работа компонента в web 

Чтобы при отладке видеть, есть ли php ошибки, добавьте в системный файл /index.php следующие строки:

ini_set('display_errors', 1);
ini_set('error_reporting', -1);

Если необходимо реализовать доступ к разным процессорам исходя из указанных параметров, можно реализовать распределение подобным образом:

if (empty($_REQUEST['action'])) {
    die('Access denied');
} else {
    $action = $_REQUEST['action'];
}

switch ($action) {
    case 'web/vote':
        // нужное действие
        break;
    case 'web/save':
        // другое действие
        break;
    default:
        $message = $_REQUEST['action'] != $action
            ? 'sample_err_register_globals'
            : 'sample_err_unknown';
        $data = json_encode(array(
            'success' => false,
            'message' => $modx->lexicon($message),
        ));
}

Работа компонента в админ. панели (CMP — Custom Manager Pages)

Документация по MODExt, расширению ExtJS, предоставляющему дополнительные функции для работы с админкой MODX. 

Чтобы при отладке видеть, есть ли php ошибки, добавьте в системный файл /manager/index.php следующие строки:

ini_set('display_errors', 1);
ini_set('error_reporting', -1);

Контроллеры CMP

Контроллер — это специальный php файл, лежащий в директории core компонента, и наследующий класс modManagerController. Его назначение — подготовить нужные файлы для вывода страницы. Иными словами, нажатие на пункт меню какого-нибудь приложения, вызывает соответствующий контроллер, который запускает ряд указанных в нем действий.

Устоявшиеся названия файлов контроллеров:  index.class.php или controller_name.class.php, но в более ранних версиях MODX, или других особых случаях они могут отличаться.

К контроллеру можно обратиться по адресу: 

/manager/?a={controller}&namespace={namespace}

  • {controller} — имя контроллера (php файла, например index (index.class.php)). У компонента может быть несколько контроллеров, к примеру основной index в корне, и пара дополнительных в папке controllers.
  • {namespace} — пространство имен созданное для компонента.

При этом, система будет искать внутри контроллера класс {namespace}{controller}ManagerController. В случае если этого класса в файле нет, появится ошибка.

До версии MODX 2.3 использовалась другая система, основанная на modAction, при этом, ссылка на страницу компонента выглядела так: manager/?a=число, и система искала в контроллере следующую конструкцию:

// Подключаем контроллер по умолчанию, из папки controllers
class IndexManagerController extends modExtraManagerController {
    public static function getDefaultController() {
        return 'home'; // указание имени файла, который подключается по умолчанию
    }
}

С новой системой, такая схема не сработает. 

Основные методы контроллеров

  • getPageTitle — выводит текст для тега title страницы CMP;
  • getTemplateFile — метод отдаёт html шаблон страницы, который будет распарсен Smarty. Учитывая, что вся админка сделана на ExtJS, метод просто выводит тег, который будет использован для отрисовки компонента ExtJs. Файл-шаблон /core/components/component_name/elements/templates/home.tpl выглядит так: 
    <div id="sendex-panel-home-div"></div>
  • getLanguageTopics  метод возвращает массив словарей, которые будут использованы в работе компонента;

  • checkPermissions — проверять или нет права на доступ к странице. Можно указать их в настройке меню, чтобы закрыть страницу от кого-то. Например, можно указать save_document, чтобы пускать только юзеров с правом редактирования документов.

  • loadCustomCssJs — основной метод контроллера, именно он загружает все нужные скрипты и стили для работы страницы. 

Отладка при работе с контроллерами

Проверить срабатывают ли изменения при синхронизации проекта с сервером можно вставив сразу после <?php следующую строку:

<?php
echo 'Hello world';die;

Добавление страницы компонента в меню Extras

Начиная с версии MODX 2.3 изменился способ создания меню. Из админ панели меню можно создать в одноименном пункте, в системном меню. 

  1. Нажимаем создать новое.
  2. В строке Parent указываем — Extras.
  3. Заполняем Lexicon Key — sample.menu_title (предварительно добавив такой ключ в лексические файлы core/components/sample/lexicon/ ).
  4. В строке Action указываем ключ коннектора, к которому должен обращаться пункт меню. В нашем случае index.
  5. В строке Namespace указываем пространство имен компонента. В нашем случае sample.

В результате в меню появится пункт, с адресом /manager/?a=index&namespace=sample, увидев этот адрес, MODX будет искать коннектор с именем index.class.php в каталоге sample.

Описание строк окна меню:

  • parent — родительский пункт меню (по умолчанию components).
  • lexicon key — текст или лексическое ключ, который должен выводиться в меню.
  • action — имя коннектора, к которому должен обращаться пункт меню. 
  • description — описание пункта меню.
  • parameters — дополнительные параметры, которые должны быть добавлены к url. 
  • handler — если установлен, то вместо обращения к конектору, будет выполнен указанный JavaScript код.
  • namespace — пространство имен, к которому привязан пункт.
  • permission — указывается уровень доступа, при котором виден этот пункт.
  • icon — html символы, отвечающие за вид вывода меню. Может указываться как иконка так и текст.

Порядок отображения устанавливается путем перетаскивания пунктов в списке.

ExtJs и каталог mgr

ExtJS — это javascript фреймфорк, куда более мощный чем jQuery, он не требует HTML верстки, а генерирует ее сам, на лету. Но, применение этого фреймворка в качестве языка для интерфейса админ-панели cms зарекомендовало себя не лучшим образом, по большей части из-за редкости применения, поэтому в сообществах стабильно возникает вопрос о переписывании этой части админки. Тем не менее, на данный момент используется именно этот фреймворк.

Основные моменты для понимания ExtJS:

  • Все элементы ExtJS являются объектами javascript, со своими свойствами. Принцип похож на Объектно-Ориентированное Программирование (ООП) в PHP. Когда вы пишите какой-то свой виджет, нужно просто унаследовать и расширить уже готовый от авторов ExtJS или MODX. Например:
    • пишите свою таблицу — расширяете MODx.grid.Grid
    • в таблицах будут всплывающие окошки — пишете виджет, расширяющий MODx.Window
  • Практически все видежты или уже работают, или умеют работать через Ajax. Все свои данные они отправляют на сервер без перезагрузки страницы, и ответ получают в формате JSON.

Логику для панели менеджера можно написать и в одном js файле, но, ниже показана структура, которая позволяет писать легко модифицируемые и дополняемые компоненты, без повторения блоков кода.

assets/components/component_name/js/mgr/component_name.js — тут объявляется объект компонента.

// В этом файле происходит создание главного объекта, его привязка к xtype и группам

// Создаём компонент - главный объект
var Sample = function(config) { // Создаём объект Sample 
    config = config || {};        // Определяем настройки объекта
    Sample.superclass.constructor.call(this,config); // запускаем конструктор родительского класса
};

// Расширяем объект MODx.Component нашим объектом
Ext.extend(Sample,Ext.Component,{
    page:{},window:{},grid:{},tree:{},panel:{},combo:{},config: {},view: {} // Перечисляем группы, внутрь которых будем "складывать" объекты
});

// Регистрируем xtype для нашего объекта
Ext.reg('sample',Sample);

// Cоздаем экземпляр нашего класса,
// чтобы можно было обращатсья к его свойствам
Sample = new Sample();

Далее переходим в файл assets/components/component_name/js/mgr/section/index.js, для настройки вывода панели на странице:

// В этом файле происходит определение блоков, которые должны выводиться на странице, 
// и добавление xtype, для вывода на экран.

Ext.onReady(function() {
    MODx.add({ // Выводим xtype страницы на странице. Ниже создаем его.
        xtype: 'sample-page-home'
        // можно указать имя дива, который указан в шаблоне, загружаемом контроллером.
        //,renderTo: 'sample-panel-home-div'
    });
});

// Устанавливаем параметры для объекта page.home
Sample.page.Home = function(config) {
    config = config || {};
    Ext.applyIf(config,{
        components: [{
            xtype: 'sample-panel-home' // указываем xtype виджета, который будем выводить. Его создадим в отдельном файле.
        }]
    });
    Sample.page.Home.superclass.constructor.call(this,config); // запускаем конструктор родительского класса
};
Ext.extend(Sample.page.Home,MODx.Component); // Объект страницы расширяет объект MODx.Component
Ext.reg('sample-page-home',Sample.page.Home); // Регистрируем новый xtype для страницы

Далее переходим в файл assets/components/component_name/js/mgr/widgets/home.panel.js 

// В этом файле происходит настройка блока с информацией, в данном случае - панели home.
// Подобное деление на отдельные виджеты необходимо для гибкой настройки отображения информации на страницах,
// К примеру один и тот же виджет можно использовать на разных страницах, либо использовать несколько виджетов на странице.

// Создаём внутри компонента главную панель
// (через точку в JS обозначется вложенность массива, то есть мы создаём
// объект Home внутри panel, который, в свою очередь, находится в Sample)
Sample.panel.Home = function (config) {
    config = config || {};
    Ext.apply(config, {
        // внутри MODx.Panel, параметры которого унаследуются, xtype уже указан, 
        // поэтому его можно пропустить, если, конечно, вы не хотите его изменить
        items: [{
            html: '<h2>Sample</h2>'
        } ]
    });
    Sample.panel.Home.superclass.constructor.call(this, config); // запускаем конструктор родительского класса для вложенности Sample.panel.Home
};
Ext.extend(Sample.panel.Home, MODx.Panel); // Панель расширяет объект MODX.Panel
Ext.reg('sample-panel-home', Sample.panel.Home); // Регистрируем новый xtype для панели

assets/components/component_name/js/mgr/widgets/component_name.grid.js — виджет с таблицой предметов modExtra. Файл, который выводит записи из БД и позволяет редактировать их во всплывающих окошках. 

Sample.grid.Names = function (config) { // Придумываем название, например, «Names»
    config = config || {};
    Ext.apply(config, {
        // Сюда перемещаем все свойства нашей таблички
        columns: [
            {dataIndex: 'id', width: 330, header: 'ID'},
            {dataIndex: 'name', width: 670, header: 'Name'}
        ],
        autoHeight: true,
        viewConfig: {
            forceFit: true,
            scrollOffset: 0
        },
        store: new Ext.data.ArrayStore({
            fields: ['id','name'],
            data: [
                [1, 'Pencil'],
                [2, 'Umbrella'],
                [3, 'Ball'],
            ]
        })
    });
    Sample.grid.Names.superclass.constructor.call(this, config); // запускаем конструктор родительского класса
}
Ext.extend(Sample.grid.Names, Ext.grid.GridPanel); // Табличка расширяет GridPanel, значит данные беруться из параметра store
Ext.reg('sample-grid-names', Sample.grid.Names); // Регистрируем новый xtype

Ext.extend(Sample.panel.Home, MODx.Panel); // Если панель расширяет объект MODX.Panel, значит данные запрашиваются у сервера.

Для того чтобы получать данные с сервера, вмето параметра store, надо указать адрес коннектора, процессора и нужные поля.

// ...
url: Sample.config.connector_url,
action: 'mgr/sample/getlist',
fields: ['id','name']
// ...

// Расширение объекта MODx.grid.Grid подразумевает что данные тянуться с сервера.
Ext.extend(Sample.grid.Names, MODx.grid.Grid); 

Список реализованных в MODX стандартных ExtJS-объектов можно увидеть по адресу /manager/assets/modext/widgets/

Процессоры

Процессоры компонента лежат в каталоге process/ Процессор это php файл, который содержит класс, наследующий modProcessor и возвращающий своё имя. В MODX уже есть несколько готовых классов процессоров, на разные случаи жизни, которые можно наследовать для своих целей (modProcessor и эти классы можно найти по адресу core/model/modx/modprocessor.class.php):

  • modObjectGetProcessor — получает и возвращает указанный объект.
  • modObjectGetListProcessor — выводит массив объектов.
  • modObjectCreateProcessor — создаёт новый объект.
  • modObjectUpdateProcessor — обновляет существующий объект.
  • modObjectDuplicateProcessor — делает копию существующего объекта.
  • modObjectRemoveProcessor — удаляет объект.
  • modObjectSoftRemoveProcessor — безопасное удаление объекта.
  • modObjectExportProcessor — экспорт объектов.
  • modObjectImportProcessor — импорт объектов.

Эти классы наследуют modObjectProcessor, который наследует modProcessor.

Методы класса modProcessor

  • setPath($path) — устанавливает путь к процессору.
  • checkPermissions() — проверяет доступ к вызываемому процессору.
  • initialize() — инициализирует процессор.
  • getLanguageTopics() — указывается массив словарей, которые нужно будет инициализировать перед выполнением процессора.
  • success($msg = '',$object = null) — возвращает успешный ответ выполнения процессора (параметр success в ответе при этом содержит true).
  • failure($msg = '',$object = null) — возвращает ошибку.
  • hasErrors() — проверяет есть ли ошибка в процессоре или нет. Поскольку у процессоров нет собственного объекта обработки ошибок, если какой-либо процессор выдал ошибку, все последующие процессоры при проверке $this->hasErrors() будут тоже возвращать ошибку. Чтобы этого избежать, можно после вызова процессора выполнять сброс ошибок $modx->error->reset().
  • addFieldError($key,$message = '') — добавляет множественное сообщение об ошибке (удобно, к примеру, при проверке нескольких полей формы).
  • process() — абстрактный метод, для описания логики работы процессора. Если ваш процессор наследует непосредственно modProcessor, или класс в котором не переопределяется данный метод, то обязательно надо прописать собственный метод process().
  • getProperty($k,$default = null) — получает свойство процессора.
  • setProperty($k,$v) — устанавливает свойства процессора, добавляя в массив $this->properties переменные со своими значениями (за один вызов одно значение).
  • unsetProperty($key) — удаляет свойство процессора.
  • setProperties($properties) — устанавливает сразу несколько свойств процессора.
  • getProperties() — получает все параметры процессора возвращая массив $this->properties.
  • setDefaultProperties(array $properties = array()) — устанавливает параметры «по умолчанию». Имеет меньший приоритет чем setProperties.
  • run() — метод в котором описывается порядок работы методов класса modProcessor. Как правило не переопределяется.
  • setCheckbox($k,$force = false) — вспомогательный метод для обработки чекбоксов.
  • outputArray(array $array,$count = false) — возвращает массив объектов, конвертированный в JSON.
  • toJSON($data) — преобразует PHP в JSON.
  • _encodeLiterals(&$value, $key) — кодирует определенные литеральные строки JavaScript для последующего декодирования.
  • _decodeLiterals($string) — декодирует строки, закодированные _encodeLiterals, для восстановления литералов JavaScript.
  • processEventResponse($response,$separator = "\n") — обрабатывает ответ от вызова события плагина.
  • getInstance(modX &$modx,$className,$properties = array()) — возвращает настоящий экземпляр производного класса. Это может быть использовано для переопределения того, как MODX загружает процессор класса; например, при обработке производных классов с настройками class_key.

Кастомный файл-процессор должен состоять из класса, и обязан в конце вернуть своё имя. Простейший пример:

<?php
class SampleNameCreateProcessor extends modObjectCreateProcessor {
	// Тип объекта, с которым будем работать - это имя выводится в логе операций MODX
	// и подставляется в сообщения об ошибках
	public $objectType = 'sampleName';
	// xPDO класс объекта
	public $classKey = 'sampleName';
	// Используемый лексикон - нужен для вывода ошибок
	public $languageTopics = array('sample');
	// Разрешение, которое проверяеся при работе с процессором
	// Можно оставить пустым - тогда смогут работать все менеджеры
	public $permission = 'new_document';
}
// Обязательно возвращаем имя класса, чтобы MODX знал, что именно нужно инициализировать в процессоре
return 'SampleNameCreateProcessor';

Процессоры можно вызвать через $modx->runProcessor(), передав нужные параметры. Чтобы узнать, какие методы можно переопределить, достаточно посмотреть в исходник наследуемого modObjectCreateProcessor

Информацию о работе некоторых процессоров можно найти в статье посвященной MODX API.

Если столкнулись с проблемой когда вместо вашего класса система ищет класс с именем modObject — укажите правильный $classKey.

P.S. Вы сэкономите себе уйму времени, если будете использовать максимально стандартизированные названия классов. Например для обьекта sampleName, использовать класс с названием sampleNameCreateProcessor, или sampleNameUpdateProcessor, и т. д.

Связь компонента и таблицы БД

Модель работы с БД в MODX Revo состоит из набора файлов php, включающих в себя основные объекты и расширения для конкретной БД. В примере будет использован MySQL. Для создания новой таблицы в MySQL используйте:

CREATE TABLE `modx_sample_names` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `description` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
    
    PRIMARY KEY (`id`),
    INDEX (`active`)
) ENGINE=MyISAM CHARSET=utf8 COLLATE utf8_general_ci;

Незабудьте указать свои параметры.

Больше информации о работе с таблицами БД.

Для создания связи с БД можно использовать xml схему, и преобразовать ее, а можно создать нужные файлы вручную. Если вам интересно собрать все вручную, взаимосвязь файлов описана после описания схемы.

XML схема таблиц

Для автоматической генерации модели работы с БД используется схема таблиц, созданная с помощью XML файла, в котором описаны все объекты и их связи. Схема может изменяться, по мере развития дополнения, не нужно пытаться предусмотреть сразу все колонки в таблицах — их можно добавить их в любое время.

Сама принцип схемы придуман для того, чтобы можно было быстро адаптировать данные под разные типы БД.

Схема хранится по адресу core/components/sample/model/schema/sample.mysql.schema.xml

Объекты

Каждый объект описывается в теге object. В атрибутах объекта указывается его имя и таблица БД, в которой он будет храниться. Таблица указывается без префикса сайта — он будет добавлен автоматически самим MODX.

<object class="smplObjectName" table="sample_object_table_name" extends="xPDOSimpleObject">

В схеме не указывается id у объектов, т.к. предполагается расширять уже существующий объект MODX, для этого используется xPDOSimpleObject.

Если использовать xPDOObject — придется самостоятельно все прописывать.

Поля объектов

<field key="name" dbtype="varchar" precision="100" phptype="string" null="false" default="" />
<field key="description" dbtype="text" phptype="text" null="true" default="" />
<field key="active" dbtype="tinyint" precision="1" phptype="boolean" attributes="unsigned" null="true" default="1" />
  • key — имя поля
  • dbtype — тип поля в базе данных: int, varchar, text и т.д.
  • precision — точность, или размер поля. Требуется для типов с фиксированной длиной, как например int и varchar. У полей text не указывается.
  • phptype — тип переменной в php, xPDO будет менять значение согласно ему: integer, string, float, json, array. Обратите внимание, что json и array — это изобретение MODX.
    Array — это для сериализованных данных, с сохранением типа, а json — обычный json. При сохранении такого поля его значение будет прогоняться через serialize() или json_encode(), a при получении — через unserialize() и json_decode.
    Таким образом, можно удобно хранить массивы в базе данных.
  • null — может ли поле быть пустым? Если вы укажите здесь false, а при работе с объектом не пришлете значение — будет ошибка в логе.
  • default — значение по умолчанию, будет использовано, если поле может быть null, и для него нет данных при сохранении
  • attributes — дополнительные свойства для передачи в БД. Они точно такие же, как в mySql.

Индексы

Ускоряют работу таблицы.

<index alias="name" name="name" primary="false" unique="false" type="BTREE">
	<column key="name" length="" collation="A" null="false" />
</index>
<index alias="active" name="active" primary="false" unique="false" type="BTREE">
	<column key="active" length="" collation="A" null="false" />
</index>
  • primary — является ли индекс первичным? Обычно — нет, первичный индекс, как правило, по полю id от xPDOSimpleObject.
  • unique — является ли индекс уникальным? То есть, может ли быть в таблице 2 и более одинаковых значения у этого поля? Уникальный индекс обычно по колонке id.

Отношение объектов

<composite alias="Subscribers" class="smplObjectName" local="id" foreign="sample_id" cardinality="many" owner="local" />
<aggregate alias="Template" class="modTemplate" local="template" foreign="id" cardinality="one" owner="foreign" />
<aggregate alias="Snippet" class="modSnippet" local="snippet" foreign="id" cardinality="one" owner="foreign" />
  • Composite — объект является главным, по отношению к другому. При удалении такого объекта будут удалены все дочерние объекты, связанные с ним здесь.
  • Aggregate — объект подчинён другому объекту. При его удалении главному ничего не будет.
    • alias — псевдоним связи. Используется в $object->getMany('Subscribers'); или $object->addOne('Template');
    • class — настоящее имя класса, с которым связывается текущий объект.
    • local — поле текущего обхекта, по которому идёт связь.
    • foreign — поля объекта, с которым связываемся.
  • Cardinality — тип связи. Один к одному, или один к нескольким. Обычно у связи aggregate это one, а у composite — many, то есть, у родителя много потомков, а у потомков только один родитель. Но бывают и исключения.

Если связь many, то использует addMany() и getMany(), если one — то addOne() и getOne().

Итоговая схема выглядит примерно так:

<?xml version="1.0" encoding="UTF-8"?>
<model package="sample" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" phpdoc-package="sample" version="1.1">

	<object class="sampleName" table="sample_object_table_name" extends="xPDOSimpleObject">
		<field key="name" dbtype="varchar" precision="100" phptype="string" null="false" default="" />
		<field key="description" dbtype="text" phptype="text" null="true" default="" />
		<field key="active" dbtype="tinyint" precision="1" phptype="boolean" attributes="unsigned" null="true" default="1" />

		<index alias="name" name="name" primary="false" unique="false" type="BTREE">
			<column key="name" length="" collation="A" null="false" />
		</index>
		<index alias="active" name="active" primary="false" unique="false" type="BTREE">
			<column key="active" length="" collation="A" null="false" />
		</index>

		<composite alias="Subscribers" class="smplObjectName" local="id" foreign="sample_id" cardinality="many" owner="local" />
		<aggregate alias="Template" class="modTemplate" local="template" foreign="id" cardinality="one" owner="foreign" />
		<aggregate alias="Snippet" class="modSnippet" local="snippet" foreign="id" cardinality="one" owner="foreign" />
	</object>


	<object class="sampleAnotherName" table="sample_aother_table" extends="xPDOSimpleObject">
		<field key="sample_id" dbtype="int" precision="10" phptype="integer" attributes="unsigned" null="false" default="0" />
		<field key="user_id" dbtype="int" precision="10" phptype="integer" attributes="unsigned" null="true" default="0" />
		

		<index alias="key" name="key" primary="false" unique="true" type="BTREE">
			<column key="sample_id" length="" collation="A" null="false" />
			<column key="user_id" length="" collation="A" null="false" />
		</index>

		<aggregate alias="Sample" class="sampleName" local="sample_id" foreign="id" cardinality="one" owner="foreign" />
		<aggregate alias="User" class="modUser" local="user_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

</model>

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

Файлы участвующие в связи с БД

Файл core/components/sample/model/sample/metadata.mysql.php — в том файле создаем xPDO-модель, чтобы рассказать MODX о существовании нашей таблицы.​​​ Для создания нескольких таблиц в рамках одного дополнения, все таблицы прописываются в этом файле.

<?php
$xpdo_meta_map = array(
    'xPDOSimpleObject' => // Объект будет расширять xPDOSimpleObject
        array(
            0 => 'sampleName', // Название класса для объекта таблицы БД
         // 1 => 'nextSampleName', // Если объектов несколько, добавляем в массив 
        ),
);

Файл core/components/sample/model/sample/samplename.class.php — создаем класс для объекта таблицы, например класс создающий элемент. 

<?php
class sampleName extends xPDOSimpleObject {
    public function create($name){
        // Пытаемся создать новый элемент
        $item = $this->modx->newObject('sampleName');
        $item->set('name',$name);
        if(!$item->save()){
            return FALSE;
        }
        return $item;
    }
}

Файл core/components/sample/model/sample/mysql/samplename.map.inc.php — карта, описывающая все поля таблицы.

<?php
// Указываем, к какому объекту относится описание
$xpdo_meta_map['sampleName'] = array(
    'package' => 'sample', // имя компонента
    'version' => '1.1',
    'table' => 'sample_names', // имя соответствующей таблицы
    'extends' => 'xPDOSimpleObject',
    'fields' => // Список полей объекта
        array(
            'name' => '',
            'description' => '',
            'active' => 1,
        ),
    'fieldMeta' =>
        array( // Описываем свойства каждого поля
            'name' =>
                array(
                    'dbtype' => 'varchar', // Тип поля в БД
                    'precision' => '255', // Длина поля
                    'phptype' => 'string', // Тип поля в PHP
                    'null' => false, // Допустимо ли значение NULL
                    'default' => '', // Значение по умолчанию
                ),
            'description' =>
                array(
                    'dbtype' => 'text',
                    'phptype' => 'string',
                    'null' => true,
                    'default' => '',
                ),
            'active' =>
                array(
                    'dbtype' => 'tinyint',
                    'precision' => '1',
                    'phptype' => 'boolean',
                    'null' => true,
                    'default' => 1,
                ),
        ),
);

Файл core/components/sample/model/sample/mysql/samplename.class.php — класс для объекта связи с таблицей. 

<?php
require_once (dirname(dirname(__FILE__)) . '/samplename.class.php');
class sampleName_mysql extends sampleName {}

Список классов modExtra

  • modExtraManagerController — заготовка страницы админки. Класс расположен в файле по адресу /core/model/modx/modmanagercontroller.class.php; Все его методы можно посмотреть там. Метод getDefaultController() по умолчанию вызывает Index

Ссылки

Хорошие статьи о написании компонентов от других авторов:

Шаблоны действий

Подключение js и css файлов из папки assets:

// Старый метод
$this->modx->regClientCSS()
$this->modx->regClientStartupScript()
$this->modx->regClientScript()
$this->modx->regClientStartupHTMLBlock()

// современный метод (работает с ajaxmanager)
$this->addCss()
$this->addJavascript()
$this->addLastJavascript()
$this->addHtml()

// для избежания проблем с кешированием во время отладки, можно добавить штамп времени в подключение
$this->addJavascript($this->samplename->config['jsUrl'].'mgr/samplename.js?time='.time());

 Разные конструкции:

// Передаем опции настроек сниппета в переменную
$tpl = $modx->getOption('tpl',$scriptProperties,'chunkName');

// заносим данные в чанк
$chunk = $this->modx->newObject('modChunk');
$chunk->set('name',$name);

// Перебираем массив элементов, перемешивая данные с тегами из шаблона
foreach ($doodles as $doodle) {
    $doodleArray = $doodle->toArray();
    $out .= $dood->getChunk($tpl,$doodleArray);
}