Jump to main content Jump to doc navigation

В этом руководстве рассказывается, как использовать MODX для создания пользовательской таблицы базы данных и связывания её с объектной моделью xPDO. К концу вы научитесь писать простую XML-схему для таблицы, создавать PHP-классы и читать и записывать данные через MODX.

Что изменилось в MODX 3?

MODX 3 приносит пространства имён PHP в XML-схему и файлы классов, а также новую функциональность bootstrap для загрузки зависимостей. Меняется и структура map-файлов MySQL в генерируемой модели. Пройдём простой пример как отправную точку. Дальше можно усложнять: конфигурационные файлы, вспомогательные функции. Сначала посмотрим минимум для создания пользовательской таблицы и покажем, что из неё можно читать и писать.

[!WARNING] Эта методология не обратно совместима с MODX 2.x. Руководство рассчитано на MODX 3. Эквивалент для 2.x: Using Custom Database Tables. Краткий чеклист апгрейда (Composer, metadata.mysql.php, instanceof, addPackage): xPDO 3.

ПРИМЕЧАНИЕ: материал переработан из прежнего примера StoreFinder.

Термины

Ниже повторяются три имени. Каждое задаётся в своём месте и имеет своё правило регистра:

Имя Где задаётся Регистр
PHP Namespace Атрибут package схемы. MODX подставляет объявление в начало каждого сгенерированного файла класса в src/Model/. PascalCase
Namespace компонента Запись namespace, которую вы создаёте в менеджере. Из неё MODX берёт Core Path и Assets Path, а пункты меню используют её, чтобы направить запрос на нужную страницу менеджера. нижний регистр
Имена файлов и каталогов Ваше собственное дерево исходников. нижний регистр, кроме каталогов под src/, которые зеркалят имена классов

Путь файла и соответствующий namespace совпадают по именам:

Файл PHP Namespace
{core_path}/components/mycomponent/src/Model/MyClass.php MyComponent\Model\MyClass

Изменения в структуре каталогов

MODX 3 добавляет Composer, PHP namespace и каталоги исходников. Каталог src/ MODX использует в собственной структуре и он готовит к Composer и другим возможностям, если компонент усложнится.

Структура в этом руководстве короче и функциональнее. Она чуть отличается от старых примеров. Раньше имя пакета часто повторялось в пути, например doodles/core/components/doodles/model/doodles. Для одного пути это лишнее.

Если вы уже работали с MODX, знаете: /core/components/: место установки пользовательских компонентов (Extras). Если не планируете публикацию как Extra, структуру можно упростить. Для публикации позже, возможно, придётся рефакторить код.

В примере создаём папку проекта в корне сайта и используем /core/components/, чтобы потом собрать пакет. Каталог project1 лежит в корне веб-сайта:

project1/
  _build/
  core/
    components/
      todo/
        src/
          Model/
        schema/
        bootstrap.php

Создайте эти каталоги, кроме src/Model/: его создаст скрипт сборки схемы build.schema.php. Если бы мы делали Custom Manager Page (CMP), понадобилась бы папка assets. Здесь фокус на данных, поэтому CMP и assets пока опускаем.

Также в списке есть bootstrap.php. Подробности: в документации, ниже разберём на примере.

Файл даёт другой способ загрузки классов через addPackage для модели. Через него доступен автозагрузчик MODX и регистрация дополнительных PSR-4 namespace с каталогом src/.

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

Подробнее: Namespaces and Bootstrapping Services

Данные и таблицы

Возьмём классический пример списка дел. Первое дело: «Собрать список дел в MODX 3!». Опишем таблицы и структуру.

Родительская таблица To-Do List и дочерняя To-Do Tasks. От списка к задачам: связь один ко многим.

Таблица Поля
modx_td_list name, short_description
modx_td_task list — внешний ключ на родительский список, task_description, due_date

После определения структуры создадим файл схемы модели. Схема: XML-представление таблиц БД. xPDO разбирает его в PHP-карты: массивы схемы и связей.

Определение XML-схемы

Структура схемы для MODX 3 существенно не менялась. Ключевое: как используется атрибут package и как он связан со сборкой. Расширяемые классы теперь с namespace: xPDOSimpleObject стал xPDO\Om\xPDOSimpleObject. Для объектов xPDO добавляйте xPDO\Om\, для MODX, например modElement,: MODX\Revolution\.

Расположение файлов MODX и xPDO

Оба дерева зеркалят структуру «класс — каталог», поэтому любой класс ищется по пути.

Префикс MODX соответствует core/src/ и дальше зеркалит структуру. Путь к modElement: core/src/Revolution/modElement.php. Если класс неочевиден, смотрите документацию и структуру каталогов.

xPDO лежит в core/vendor/xpdo/xpdo/src/xPDO/, дальше: как у класса. Путь к xPDOSimpleObject: core/vendor/xpdo/xpdo/src/xPDO/Om/xPDOSimpleObject.php.

Содержимое файла схемы

Ниже две таблицы и связь родитель/потомок через composite/aggregate. Записи задают владельца и тип связи. При удалении списка с 20 задачами удалятся и список, и задачи.

Подробнее о выборе xPDOObject и xPDOSimpleObject: Comparing xPDOObject and xPDOSimpleObject. Здесь используем xPDOSimpleObject, чтобы получить автоинкрементный первичный ключ id.

Создайте в каталоге schema файл todo.mysql.schema.xml. Полный путь от project1: project1/core/components/todo/schema/todo.mysql.schema.xml. Скопируйте XML ниже и сохраните.

<?xml version="1.0" encoding="UTF-8"?>
<model package="ToDo\Model" baseClass="xPDO\Om\xPDOObject" platform="mysql"  defaultEngine="InnoDB" version="3.0">
	
	<object class="tdList" table="td_list" extends="xPDO\Om\xPDOSimpleObject">
		<field key="name" dbtype="varchar" precision="128" phptype="string" default="" />
		<field key="short_description" dbtype="varchar" precision="255" phptype="string" default="" />

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

		<composite alias="Task" class="ToDo\Model\tdTask" local="id" foreign="list" cardinality="many" owner="local" />
	</object>

	<object class="tdTask" table="td_task" extends="xPDO\Om\xPDOSimpleObject">
		<field key="list" dbtype="int" precision="10" phptype="integer" null="false" default="" />
		<field key="task_description" dbtype="varchar" precision="255" phptype="string" default="" />
		<field key="due_date" dbtype="datetime" phptype="datetime" null="true" default="NULL" />

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

		<aggregate alias="List" class="ToDo\Model\tdList" local="list" foreign="id" cardinality="one" owner="foreign" />
	</object>

</model>

[!WARNING] В MODX 3 в aggregate и composite атрибут class должен содержать полное имя класса с namespace.

Тег model и атрибуты задают свойства компонента/Extra:

Атрибут Значение
package PHP namespace пакета xPDO. Важное изменение MODX 3: это значение попадает в PHP-файлы модели как объявление namespace.
baseClass Базовый класс для всех определений. Если не планируете своё расширение xPDOObject, оставьте по умолчанию.
platform MySQL (mysql).
defaultEngine Движок таблиц по умолчанию, InnoDB или MyISAM. Сам MODX использует InnoDB.
version Оставьте 3.0. Это не косметика: начиная с 3.0 генератор пишет Model/metadata.mysql.php с картой классов, и только модель с этой картой доходит до PSR-4 автозагрузчика. С version="1.0" классы генерируются, но addPackage() их не подхватывает.
phpdoc-package MODX напрямую не использует, но можно в Documentation Blocks. Ожидается PHP namespace в MODX 3. Если не задать, MODX подставит сам. В docblock классов будет @package MyComponent\Model.
phpdoc-subpackage По phpDocumentor устарело. Namespace уже объединяет package и subpackage.

Подробнее о схемах MODX: Defining a Schema | Database and Tables, где разобраны атрибуты Class, Table и Extends.

О путях: Namespaces | Usage.

Запись namespace компонента в менеджере

Данные записи namespace компонента те же, что в MODX 2 — см. раздел о терминах. MODX 3 добавляет ещё одно применение namespace компонента, об этом в следующем разделе.

Откройте Namespaces и создайте новую запись.

Заполните поля и сохраните запись. Имя «Name» и оба пути должны быть в нижнем регистре, оба пути — заканчиваться слэшем.

Name: todo Core Path: {base_path}project1/core/components/todo/ Assets Path: {base_path}project1/assets/components/todo/

Что такое base_path? {base_path} MODX подставляет базовый путь установки. Это корень сайта или подкаталог, если MODX установлен не в корень.

Как MODX использует запись namespace компонента?

С MODX 3 core path в записи namespace: место, где MODX ищет bootstrap.php. Bootstrap подключает классы, автозагрузчик, сервисы и другие задачи при старте.

Когда выполняется bootstrap.php

index.php вызывает функцию инициализации с ключом контекста web, если только config.context.php в корне установки не вернёт другой. Эта функция в modX.php сначала вызывает _initNamespaces(), а затем _initContext(). _initNamespaces() читает записи namespace из кеша, перебирает их и подключает каждый найденный bootstrap.php.

[!WARNING] bootstrap.php подключается до _initContext(), поэтому внутри него контекста ещё нет: $modx->getContext() вернёт null, а настройки контекста ещё не в конфигурации. Ограничивайтесь самим $modx и данными namespace в файле bootstrap — всё остальное ждёт своего момента в запросе.

Скрипт сборки схемы

Создайте build.schema.php в _build/. Содержимое ниже. В другой документации пишут, что «MODX_API_MODE» устарел. Похоже, это не так: константа есть в сборке 3.0.0-rc1. Значение true блокирует вызов $modx->handleRequest().

<?php

// Set API Mode to true
define('MODX_API_MODE', true);

// Include the main index.php file to load MODX in API Mode
@include(dirname(__FILE__, 3) . '/index.php');

/**
 * @var \MODX\Revolution\modX $modx
 * 
 */

// Get the manager and generator
$manager = $modx->getManager();
$generator = $manager->getGenerator();

// Define the paths needed
//{base_path}/project1/
$projectRootDir = MODX_BASE_PATH . 'project1/';

//{base_path}/project1/core/components/todo/
$corePath = $projectRootDir . 'core/components/todo/';

//{base_path}/project1/core/components/todo/schema/todo.mysql.schema.xml
$schemaFile = $corePath . "schema/todo.mysql.schema.xml";

if (is_file($schemaFile)) {
	echo("Parsing schema: $schemaFile".PHP_EOL);
	// Parse the schema to generate the class files
	$generator->parseSchema(
		$schemaFile, 
		$corePath . 'src/',
		[
			"compile" => 0,
			"update" => 0,
			"regenerate" => 1,
			"namespacePrefix" => "ToDo\\"
		]
	);
}
else {
	echo("Schema file path invalid: $schemaFile".PHP_EOL);
}

В примерах 2.x встречаются главный класс или блоки с getProperty. Здесь пути захардкожены для простоты. Если папка проекта не в корне сайта, подстройте пути.

Тайминги из скрипта убраны: для 5 и меньше таблиц сборка и так быстрая.

Шаги прокомментированы в скрипте. Подключаем index, а не класс MODX напрямую. В index есть проверка API Mode, которую мы включаем.

Запустите php build.schema.php из _build. Появится каталог src/Model/ и другие файлы:

Настройка bootstrap.php

Классы уже есть, объекты можно создавать через xPDO::newObject, но таблиц в БД ещё нет. Перед созданием таблиц настроим bootstrap, чтобы классы загружались при каждой инициализации MODX.

MODX 3 по-прежнему вызывает addPackage, формат чуть другой. Внутри addPackage namespace модели добавляется в PSR-4 автозагрузчик. Пока достаточно одной строки с addPackage.

Блок комментариев с переменными поможет редактору подсказывать функции и видеть $modx.

Создайте bootstrap.php в components/todo/ и вставьте код ниже.

<?php

/**
 * @var \MODX\Revolution\modX $modx
 * @var array $namespace
 */

// Load the classes
$modx->addPackage('ToDo\Model', $namespace['path'] . 'src/', null, 'ToDo\\');

[!WARNING] Запустите build.schema.php до того, как начнёте полагаться на этот файл. addPackage() читает src/Model/metadata.mysql.php, и когда файла нет, молча выходит без ошибки PHP — единственный след, предупреждение Could not load package metadata в журнале MODX, после чего все классы компонента дальше по запросу «не найдены».

[!CAUTION] bootstrap.php выполняется до того, как менеджер закончит загрузку. Опечатка в PHP, неверное имя класса или любой echo в этом файле ломают менеджер, и он перестаёт загружаться. Уберите лишний вывод, и всё заработает снова.

Скрипт создания таблиц

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

За рамками руководства: xPDO CLI для записи и сборки, можно включить в composer.json.

Создайте build.tables.php в _build/ и скопируйте код ниже.

<?php

// Set API Mode
define('MODX_API_MODE', true);

// Include the index to load MODX in API Mode
@include(dirname(__FILE__, 3) . '/index.php');

/**
 * @var \MODX\Revolution\modX $modx
 * 
 */

// Classes to loop through
$classes = [
	'ToDo\Model\tdList',
	'ToDo\Model\tdTask'
];

// Get the manager
$manager = $modx->getManager();

// Loop through our classes
foreach ($classes as $class) {
	// Check if the class exists
	if (class_exists($class)  ) {
		// Create the table
		echo("Creating table for class: $class".PHP_EOL);
		$manager->createObjectContainer($class);
	}
	else {
		echo("Unable to load model class: $class".PHP_EOL);
	}
}

Запустите php build.tables.php.

Если вывод похож на ниже: готово!

Tables Built Success Message

В phpMyAdmin или другом клиенте MySQL найдите таблицы с «td». Должно быть две новые.

Database Tables Generated

Использование новой модели

Два примера. Первый: модели в сниппете и запись данных. Второй: чтение и вывод. Раздел про сниппеты для фронтенда.

В следующих руководствах можно развернуть это в полноценный Extra с Custom Manager Page.

Для проверки создайте или измените ресурс с HTML ниже. Обходим сложности с шаблоном и итерацией: вывод в <pre> как лог. Покажет все списки и задачи.

Я заменил содержимое домашней страницы MODX на вызов сниппета ToDo. !: без кеша.

<pre>[[!ToDo]]</pre>

Создайте сниппет с именем ToDo. Вставьте скрипт ниже. Комментарии по ходу. Можно было разделить на два сниппета или CLI для генерации и отдельный для вывода.

Здесь всё в одном месте. Параметр &action=generate создаст списки и задачи из массива данных.

ПРИМЕЧАНИЕ: повторная загрузка с параметром action добавит дубликаты с новыми id.

Вторая половина читает данные и собирает строку. Подробнее о чтении и записи: Creating Objects, Retrieving Objects, Add Many.

<?php
// Output
$output = "";

// Namespace
$namespace = "ToDo\\Model\\";

// Handle task generation
$action = $_GET['action'] ?: "Get Lists";
$output .= "Getting ToDo List Data: Action ($action)";

if ($action === 'generate') {
    // Define our todo list data as an array
    $data = [
        [   
            "name" => "Grocery List",
            "short_description" => "Things to buy at the store",
            "tasks" => [
                ["task_description" => "Eggs", "due_date" => "2026-10-17"],
                ["task_description" => "Cheese", "due_date" => "2026-10-17"],
                ["task_description" => "Kale Chips"]
            ]
        ],
        [
            "name" => "House Projects",
            "short_description" => "Items to complete around the house",
            "tasks" => [
                ["task_description" => "Fix the bathroom sink leak", "due_date" => "2026-10-20"],
                ["task_description" => "Figure out how to fix the front door"],
                ["task_description" => "Rake up the leaves? Maybe?"]
            ]
        ]
    ];
    
    // Now let's loop through and create our lists and tasks
    foreach ($data as $createList) {
        // Get a list object and set the values
        $newList = $modx->newObject($namespace.'tdList');
        $newList->set('name', $createList['name']);
        $newList->set('short_description', $createList['short_description']);
        
        // Now before we save the list, let's create an array of task objects
        $newTaskArr = [];
        foreach ($createList['tasks'] as $createTask) {
            // Get a task object and set the values
            $newTask = $modx->newObject($namespace.'tdTask');
            $newTask->set('task_description', $createTask['task_description']);
            if (!empty($createTask['due_date'])) {
                $newTask->set('due_date', $createTask['due_date']);
            }
            
            // Add the task object to the array
            $newTaskArr[] = $newTask;
        }
        
        // Use the addMany function to associate all the tasks to the parent list
        $newList->addMany($newTaskArr);
        
        // And finally call the save function to persist the data to our tables
        $newList->save();
    }
}

// Query for any lists
$lists = $modx->getCollection($namespace.'tdList', []);
$listCount = 0;

// If our query returned results
if ($lists) {
    // Loop through them
    foreach ($lists as $list) {
        // Add the list to the output
        $output .= PHP_EOL.PHP_EOL.'(' . $list->get('id') . ') '.$list->get('name');
        
        // Add the description if we have one
        if ($list->get('short_description'))
            $output .= PHP_EOL.' - '.$list->get('short_description');
            
        // Now get any tasks
        $tasks = $modx->getCollection($namespace.'tdTask', ['list' => $list->get('id')]);
        $taskCount = 0;
        
        // If we have tasks
        if ($tasks) {
            // Loop through the tasks
            foreach ($tasks as $task) {
                // Add the task to the output
                $output .= PHP_EOL."    > " . $task->get('task_description');
                if ($task->get('due_date')) {
                    $output .= ' (' . $task->get('due_date') . ')';
                }
            }
        }
    }
}

// Return output
return $output;

На корневой или тестовой странице вывод будет похож на скриншот:

Смотрите также

Support the team building MODX with a monthly donation.

The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.

Backers

  • modmore
  • STERC
  • Digital Penguin
  • Jens Wittmann – Gestaltung & Entwicklung
  • CrewMark
  • Fabian Christen
  • Sepia River Studios
  • Dannevang Digital
  • Alex
  • A. Moreno
  • Chris Fickling
  • Stéphane Jäggi
  • Murray Wood
  • Anton Tarasov
  • JT Skaggs
  • deJaya
  • Lefthandmedia
  • eydolan
  • Following Sea
  • Guido Gallenkamp
  • YJ
  • Raffy
  • Snow Creative
  • Nick Clark
  • Guest
  • Helen
  • krisznet
  • Yanni
  • Richard

Budget

$194 per month—let's make that $500!

Learn more