MODX Custom Extra Package Development
A MODX Extra is a full-featured extension: a component (interface in manager), set of snippets, plugins, chunks, system settings. Packaged as .transport.zip for installation via Package Manager. This is the proper way to distribute and maintain complex functionality.
Extra Structure
core/components/myextra/
├── controllers/
│ └── home.class.php # MODX controller
├── docs/
│ └── changelog.txt
├── elements/
│ ├── chunks/
│ ├── plugins/
│ ├── snippets/
│ └── templates/
├── lexicon/
│ └── ru/
│ └── default.inc.php # Translations
├── model/
│ └── myextra/ # xPDO models
│ ├── myitem.class.php
│ ├── mysql/
│ │ ├── myitem.class.php
│ │ └── myitem.map.inc.php
│ └── myextra.mysql.schema.xml
└── processors/
└── mgr/
└── items/
├── getlist.class.php
├── get.class.php
├── create.class.php
├── update.class.php
└── remove.class.php
assets/components/myextra/
├── js/
│ ├── mgr/
│ │ └── myextra.js # ExtJS interface
│ └── widgets/
│ └── items.grid.js
└── css/
└── mgr.css
xPDO Database Schema
<!-- core/components/myextra/model/myextra/mysql/myextra.mysql.schema.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<model package="myextra" baseClass="xPDOObject" platform="mysql" version="1.1">
<object class="MyItem" table="myextra_items" extends="xPDOSimpleObject">
<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/>
<field key="description" dbtype="text" phptype="string" null="true"/>
<field key="price" dbtype="decimal" precision="10,2" phptype="float" null="false" default="0.00"/>
<field key="status" dbtype="tinyint" precision="1" phptype="integer" null="false" default="1"/>
<field key="created_at" dbtype="datetime" phptype="datetime" null="true"/>
<index alias="name" name="name" primary="false" unique="false" type="BTREE">
<column key="name" length="" collation="A" null="false"/>
</index>
</object>
</model>
xPDO Model
// core/components/myextra/model/myextra/myitem.class.php
class MyItem extends xPDOSimpleObject {
public function getLink(): string {
return $this->xpdo->makeUrl(
$this->xpdo->getOption('myextra.item_page', null, 1),
'',
['id' => $this->get('id')],
'full'
);
}
}
Processor (CRUD Operations)
// core/components/myextra/processors/mgr/items/getlist.class.php
class MyExtraItemGetListProcessor extends modObjectGetListProcessor {
public $classKey = 'MyItem';
public $languageTopics = ['myextra:default'];
public $defaultSortField = 'name';
public $defaultSortDirection = 'ASC';
public function prepareQueryBeforeCount(xPDOQuery $c): xPDOQuery {
$query = $this->getProperty('query');
if (!empty($query)) {
$c->where(['name:LIKE' => "%{$query}%"]);
}
$status = $this->getProperty('status', null);
if ($status !== null && $status !== '') {
$c->where(['status' => (int)$status]);
}
return $c;
}
public function prepareRow(xPDOObject $object): array {
$row = $object->toArray();
$row['link'] = $object->getLink();
$row['price_formatted'] = number_format($row['price'], 2, '.', ' ') . ' ₽';
return $row;
}
}
Extra Snippet
<?php
// core/components/myextra/elements/snippets/myextra.snippet.php
$basePath = $modx->getOption('myextra.core_path', null,
$modx->getOption('core_path') . 'components/myextra/');
$modx->addPackage('myextra', $basePath . 'model/');
$modx->lexicon->load('myextra:default');
$limit = (int)($scriptProperties['limit'] ?? 10);
$offset = (int)($scriptProperties['offset'] ?? 0);
$tpl = $scriptProperties['tpl'] ?? 'myextra.item';
$c = $modx->newQuery('MyItem');
$c->where(['status' => 1]);
$c->limit($limit, $offset);
$c->sortby('name', 'ASC');
$items = $modx->getCollection('MyItem', $c);
$output = '';
foreach ($items as $item) {
$output .= $modx->getChunk($tpl, $item->toArray());
}
return $output;
Transport Package Build
// _build/build.transport.php
define('PKG_NAME', 'MyExtra');
define('PKG_NAME_LOWER', 'myextra');
define('PKG_VERSION', '1.0.0');
define('PKG_RELEASE', 'pl');
// Package creation
$builder = new modPackageBuilder($modx);
$builder->createPackage(PKG_NAME_LOWER, PKG_VERSION, PKG_RELEASE);
$builder->registerNamespace(PKG_NAME_LOWER, false, true, '{core_path}components/' . PKG_NAME_LOWER . '/');
// Add category with elements
$category = $modx->newObject('modCategory');
$category->set('id', 1);
$category->set('category', PKG_NAME);
// Add snippets, chunks, plugins...
$snippets = includeSnippets($builder, $modx);
$category->addMany($snippets);
// Create vehicle
$attr = [
xPDOTransport::PRESERVE_KEYS => false,
xPDOTransport::UPDATE_OBJECT => true,
xPDOTransport::UNIQUE_KEY => 'category',
xPDOTransport::RELATED_OBJECTS => true,
xPDOTransport::RELATED_OBJECT_ATTRIBUTES => ['Snippets' => [...], 'Chunks' => [...]],
];
$vehicle = $builder->createVehicle($category, $attr);
$builder->putVehicle($vehicle);
// Final packaging
$builder->pack();
Timeline
Extra development with CRUD interface in manager, 2–3 snippets, and basic database schema — 2–3 weeks.







