
Habrahabr has already written a lot about creating components for the MODx Revolution, but in my opinion there is still no comprehensive manual in Russian. A lot of gaps and not very accurate information. But such a guide is
in the official documentation in English. I think in this case, amateur to anything :). Although a real programmer should not have problems with English, I think reading Russian for the majority is more pleasant. I decided to make a free translation of this fundamental, in my opinion, part of the documentation. I offer you the first part. I hope to have enough patience to finish this work, and maybe someone will help me with this. Do not worry that a lot of text, it seems difficult only at first glance.
Overview
In this lesson you will learn about the development of a simple add-on “Doodles” (blanks) that uses a custom database table to store objects called “Doodles” that have a name and description. We will create a snippet that will display a list that is formatted through a chunk, its own administration page (component) using ExtJS (part 2), and also make a script for packing into a package (part) - part 3. It will also be i18n- compatible, i.e. have files to translate into different languages. By the way, this package can be
downloaded and thoroughly studied.
Creating a directory structure
So, I created the folder
/ www / doodles / on my local server. I have this folder at
http: // localhost / doodles / . Our directory structure looks like this:

Note a few things. First, we have 3 main directories:
core / ,
assets / and
_build / . Usually MODx Revo add-ons are divided into two directories:
core / components / myextra / and
assets / components / myextra / . The
assets / components / directory contains only web-specific files — JavaScript, CSS, and so on. These are files that are publicly available on the Internet. All PHP files will be in the
core / components / directory. The
core / directory can be moved out of the webroot for additional security. These directories will be packed into our component's transport package in order to be able to quickly install from the “Package Management” section of the administrative part of the site. The
_build / directory will not be packaged in a transport package. It is needed only at the stage of creating this package. This will be discussed in the last part of the lesson.
')
In the
assets / folder we will have only one PHP file -
connector.php . This file will allow us to have access to the processors of our user management page (Custom Manager Page - CMP). More on that later.
In the
core / components / doodles / directory, create several directories:
controllers - controllers for CMP;
docs - contains only changelog, readme and license files;
elements - all our snippets, plugins, chunks, etc .;
lexicon - all i18n language files;
model - our classes, as well as the XML schema file for our user database tables;
processors are all our processors for CMP.
Creating a Snippet
Create a snippet file:
/www/doodles/core/components/doodles/elements/snippets/snippet.doodles.php . Add a few lines of code to the file:
$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 '';
Oh! What it is? This is where the magic happens. Let's break down every part. First, we have a call to the getService () method. We divide this line into several parts for easier reading:
$defaultDoodlesCorePath = $modx->getOption('core_path').'components/doodles/'; $doodlesCorePath = $modx->getOption('doodles.core_path',null,$defaultDoodlesCorePath); $doodles = $modx->getService('doodles','Doodles',$doodlesCorePath.'model/doodles/',$scriptProperties);
Well, what is
$ modx-> getOption () ? With this method you can find out our system settings.
$ modx-> getService () loads the class, creates an object instance, if it exists, and sets it to
$ modx-> doodles , in this case (the first parameter passed to the method). Read more
here .
Creating path settings
In the control system, go to “System” -> “System Settings”. Create two new parameters with the appropriate values:
doodles.core_path - {core_path} components / doodles /
doodles.assets_url - {assets_path} components / doodles /
Creating a base class
Create a class file
/www/doodles/core/components/doodles/model/doodles/doodles.class.php . This class will be useful to us because in it we can define some basic paths, as well as methods that we will use in our component.
<?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); } }
Now back to our snippet. Add a few properties:
$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 ''; $tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl'); $sort = $modx->getOption('sort',$scriptProperties,'name'); $dir = $modx->getOption('dir',$scriptProperties,'ASC'); $output = ''; return $output;
Cool. Now we want to use
xPDO for database queries to capture our records ... oh. We have not yet made an xPDO model for them. We have to do it.
Creating a model
xPDO does database abstraction into pretty OOP query methods. He is currently starting to maintain multiple databases. In addition, it allows you to turn your database strings into good, clean classes and make it all very short lines of code. But first we need to build this model using xPDO schemes.
Create an XML file
/www/doodles/core/components/doodles/model/schema/doodles.mysql.schema.xml . Put this into it:
<?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>
We will understand what it all means. First line:
<model package="doodles" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM">
contains the name of our package - "doodles". She also says that our base class will be “xPDOObject”, and that this scheme is made for MySQL. She also says that MyISAM is our default MySQL storage system. Further properties of the database table are set. You do not need to create a field with an identifier, since it is always created by the xPDOSimpleObject object and sets the 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"/>
This creates a link to other xPDO objects (in this case, the user object).
Script parsing scheme
Now it's time to pay attention to our
_build / directory. Let's create a file in it
/www/doodles/_build/build.config.php with the following content:
<?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/');
Check the correctness of all paths for your case. Now we will create the script itself, which will analyze our XML schema and create its PHP representation. Create the file
/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>'; $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();
Now you can run the
_build / build.schema.php file . I do this by downloading in a web browser:
http: //localhost/doodles/_build/build.schema.php . After this, class files and maps will be generated.

Now let's make a small adjustment to our Doodles base class (/www/doodles/core/components/doodles/model/doodles/doodles.class.php). Add the following line to array constructor immediately after array_merge:
$this->modx->addPackage('doodles',$this->config['modelPath']);
This tells xPDO that we want to add an xPDO “doodles” package, which will allow us to query our user table.
Snippet include
Earlier we created a snippet. To use it, you can now create a snippet in the management system and paste the code from the file
doodles / elements / snippets / snippet.doodles.php . But the code of such a snippet may not be convenient to edit. To fix this you can create a simple universal “include” snippet. Let's go to the control system “Elements” -> “Snippety” -> “New snippet”.
Snippet name:include
Snippet code (php): <?php return include $file;
Now on any page or in any template you can make such a snippet call:
[[!include? &file=`[[++doodles.core_path]]elements/snippets/snippet.doodles.php`]]
Creating database queries
First, we need to create a table. To do this, simply add the following lines to our snippet before returning a value (return):
$m = $modx->getManager(); $created = $m->createObjectContainer('Doodle'); return $created ? ' .' : ' .';
Now, if you run our snippet, the Doodles table will be automatically created in the database. After that, you can delete this code, and better do it like this:
$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'); }
Now add the following lines to the snippet:
$doodles = $modx->getCollection('Doodle'); $output = count($doodles);
At the exit you should see "0", because the table is still empty.
Create a couple of lines in the table (for example, using phpMyAdmin) and you will see how the snippet prints their number. You can also create rows in a table like this:
$doodle = $modx->newObject('Doodle'); $doodle->fromArray(array( 'name' => 'TestDoodle', 'description' => 'A test doodle' )); $doodle->save();
Do not forget to delete this code.
Fine! Custom database query works! Let's make it more difficult. We can use
xPDOQuery xPDO to create some fairly complex queries. For now, let's just add the sort commands:
$c = $modx->newQuery('Doodle'); $c->sortby($sort,$dir); $doodles = $modx->getCollection('Doodle',$c);
Sorting takes place in the $ sort field in the order $ dir. The value of these variables we defined above. In a call to the snippet, it will look like this:
[[!include? &file=`[[++doodles.core_path]]elements/snippets/snippet.doodles.php`&sort=`name`&dir=`DESC`]]
DoCles class getChunk method
Add a couple of helper methods to the base class:
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; }
So far, all you need to know is that these methods will look for chunks in your
/ www / doodles / core / components / doodles / elements / chunks / directory.
Create a file
/www/doodles/core/components/doodles/elements/chunks/rowtpl.chunk.tpl with something like this:
<li><strong>[[+name]]</strong> - [[+description]]</li>
In our snippet add the following lines:
foreach ($doodles as $doodle) { $doodleArray = $doodle->toArray(); $output .= $dood->getChunk($tpl,$doodleArray); }
The complete snippet code is:
<?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 ''; $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;
At the output we will see a list of our test data from the table:

So, we loaded the base class in the paths created in the system settings, retrieved the xPDO package from the user database table and derived it from the template using the chunk.
The
next part of this lesson describes how to create an administrative page (component). About it on Habré recently
told a good
bezumkin . In the
third part , in my opinion no one has written yet, maybe I’ll make a translation soon. It tells about the packaging of the entire component in the package, which can be easily installed through the "Package Management" section in the management system.