
This is the translation of the
third part of the lesson . As I wrote in the
previous article , there is no sense to translate the second part, because There is already enough information in Russian to create a component page. So I jumped to the third.
In this lesson you will learn how to pack an add-on into a
transport package , which can then be easily installed through Package Management. We will pack everything that relates to the addition developed by us: a snippet; files from core / components / and assets / components /; actions; menu item and namespace of our CMP (component pages); default values ​​for snippet with internationalization support (i18n). And also add a resolver that will create user tables in the database.
')
Reference:
For packing simple add-ons, you can use
PackMan . But in this case, we want to do it on our own and fully understand what the transport package is.
Setting the build directory
At the end of the lesson, the _build directory will look like this:

We are already familiar with the
build.config.php and
build.schema.php files from the first part of the tutorial, but for now let's just look at the other parts:
data - Here we are going to put all our scripts to pack the package data.
resolvers - The folder contains
resolvers for the transport packet.
build.transport.php - This is the main wrapper script that will need to be run to create the package.
setup.options.php - Installer settings. A brief look at what is needed later.
Creating a wrapper script
Create a file
/www/doodles/_build/build.transport.php with the following contents:
<?php $tstart = explode(' ', microtime()); $tstart = $tstart[1] + $tstart[0]; set_time_limit(0); define('PKG_NAME','Doodles'); define('PKG_NAME_LOWER','doodles'); define('PKG_VERSION','1.0'); define('PKG_RELEASE','rc1'); $root = dirname(dirname(__FILE__)).'/'; $sources = array( 'root' => $root, 'build' => $root . '_build/', 'data' => $root . '_build/data/', 'resolvers' => $root . '_build/resolvers/', 'chunks' => $root.'core/components/'.PKG_NAME_LOWER.'/chunks/', 'lexicon' => $root . 'core/components/'.PKG_NAME_LOWER.'/lexicon/', 'docs' => $root.'core/components/'.PKG_NAME_LOWER.'/docs/', 'elements' => $root.'core/components/'.PKG_NAME_LOWER.'/elements/', 'source_assets' => $root.'assets/components/'.PKG_NAME_LOWER, 'source_core' => $root.'core/components/'.PKG_NAME_LOWER, ); unset($root); require_once $sources['build'] . 'build.config.php'; require_once MODX_CORE_PATH . 'model/modx/modx.class.php'; $modx= new modX(); $modx->initialize('mgr'); echo '<pre>'; $modx->setLogLevel(modX::LOG_LEVEL_INFO); $modx->setLogTarget('ECHO'); $modx->loadClass('transport.modPackageBuilder','',false, true); $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.'/'); $modx->log(modX::LOG_LEVEL_INFO,'Packing up transport package zip...'); $builder->pack(); $tend= explode(" ", microtime()); $tend= $tend[1] + $tend[0]; $totalTime= sprintf("%2.4f s",($tend - $tstart)); $modx->log(modX::LOG_LEVEL_INFO,"\n<br />Package Built.<br />\nExecution time: {$totalTime}\n"); exit ();
There are quite a lot of things, but note that this is all that is needed to pack our namespace and create the transport package file “doodles-1.0-rc1.zip” (only the base). We will analyze in detail.
$tstart = explode(' ', microtime()); $tstart = $tstart[1] + $tstart[0]; set_time_limit(0); define('PKG_NAME','Doodles'); define('PKG_NAME_LOWER','doodles'); define('PKG_VERSION','1.0'); define('PKG_RELEASE','rc1');
First, we are going to get the start time of the assembly, so that at the end it will take how much time it took to assemble. It is not necessary at all, just useful information. Then we specify the name, version and release type. Further:
$root = dirname(dirname(__FILE__)).'/'; $sources = array( 'root' => $root, 'build' => $root . '_build/', 'data' => $root . '_build/data/', 'resolvers' => $root . '_build/resolvers/', 'chunks' => $root.'core/components/'.PKG_NAME_LOWER.'/chunks/', 'lexicon' => $root . 'core/components/'.PKG_NAME_LOWER.'/lexicon/', 'docs' => $root.'core/components/'.PKG_NAME_LOWER.'/docs/', 'elements' => $root.'core/components/'.PKG_NAME_LOWER.'/elements/', 'source_assets' => $root.'assets/components/'.PKG_NAME_LOWER, 'source_core' => $root.'core/components/'.PKG_NAME_LOWER, ); unset($root); require_once $sources['build'] . 'build.config.php'; require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
Here we define ways where to find all parts of our package for packaging.
Finally, we connected the
build.config.php file and the MODx class. Now it's time to load the MODx object:
$modx = new modX(); $modx->initialize('mgr'); echo '<pre>'; $modx->setLogLevel(modX::LOG_LEVEL_INFO); $modx->setLogTarget('ECHO'); $modx->loadClass('transport.modPackageBuilder','',false, true); $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.'/');
Here we create a modX object and initialize the context “mgr”. Next, we ask MODX to be more verbose in his error messages while running our script. Please display messages on the screen.
Then we load the “modPackageBuilder” class and get two useful methods createPackage and registerNamespace.
$modx->createPackage(key,version,release)
Here we give the name of our package (it should be in lower case and should not contain a period or hyphen), version and release type. Now modPackageBuilder will automatically pack our namespace:
$builder->registerNamespace(namespace_name,autoincludes,packageNamespace,namespacePath)
The first parameter is the namespace name ("doodles" in our case). The second is an array of classes associated with our namespace (we don’t need it, so we set it to false). By the third parameter, we say that we want to pack the namespace into a package (set to true). And the third parameter is the path to our namespace. This last parameter is key. Pay attention to the placeholder "{core_path}", it will be replaced with the actual path during the package installation, which will make the package more flexible. No need to point the way hard.
And here are the last few lines of our packer:
$modx->log(modX::LOG_LEVEL_INFO,'Packing up transport package zip...'); $builder->pack(); $tend= explode(" ", microtime()); $tend= $tend[1] + $tend[0]; $totalTime= sprintf("%2.4f s",($tend - $tstart)); $modx->log(modX::LOG_LEVEL_INFO,"\n<br />Package Built.<br />\nExecution time: {$totalTime}\n"); exit ();
The pack () method tells MODX to create a zip file for the transport package. The remaining lines simply output the time it took to assemble. That's all. If you run
this is in the browser (I have the address http: //localhost/doodles/_build/build.transport.php), you will get debug information in the
core / packages / folder as well:

This is our transport package! However, this is not enough for our supplement.
Adding data
We want to add our snippet to the package in a separate category “Doodles”. In the
build.transport.php file
, add the following code to registerNamespace below:
<?php $category= $modx->newObject('modCategory'); $category->set('id',1); $category->set('category',PKG_NAME);
First, we create a modCategory (category) object named “Doodles”. Please note that we do not save -> save (), but only create an object. Next, we have code for snipet packaging, but for the time being we ignore it, we will return to it later.
Then we created a large array of attribute attributes of the vehicle (Vehicle) category. What kind of vehicle? Well, this is the vehicle that carries the object to the transport package. Each object (snippet, menu item, category, etc.) must have a vehicle for "transportation" in the transport package. Thus, we created one of them, but initially assigned several attributes that tell MODX how this vehicle should behave when the user installs a package.
- xPDOTransport :: UNIQUE_KEY => 'category' - here we say MODX, that the “category” field is a unique key for this category.
- xPDOTransport :: PRESERVE_KEYS => false - sometimes we want the primary key of our object to be “saved”. This is useful for non-auto-incrementing keys (PKs), such as on the menu, which we will get later. Our category does not need this, so we set it to false.
- xPDOTransport :: UPDATE_OBJECT => true - this tells MODX that if a category already exists, you need to update it with our version. If set to false, MODX will just skip the category if it finds it. We want the category to be updated.
- xPDOTransport :: RELATED_OBJECTS => true - this indicates related objects (we specify the object of the snippet). Our case is a good example. Any snippets to be installed will be placed in the category.
- xPDOTransport :: RELATED_OBJECT_ATTRIBUTES - This is an associative array with attributes of related objects. In our case, this is only a snippet, but it can be plug-ins, TV-parameters (additional fields), chunks, etc.
Set properties for the snippet object:
'Snippets' => array( xPDOTransport::PRESERVE_KEYS => false, xPDOTransport::UPDATE_OBJECT => true, xPDOTransport::UNIQUE_KEY => 'name', ),
Here we say that saving the primary key is not required (similar to the category). Then we want to update the object if it already exists. Finally, we say MODX that the “name” field is the primary key.
Next, do this:
$vehicle = $builder->createVehicle($category,$attr); $builder->putVehicle($vehicle);
It packs our category object into a small vehicle with the attributes that we have just defined. This adds it to the transport package. Done! Our category is packed. Now add a snippet to it.
Add snippet
Go ahead and create the folder
/ www / doodles / _build / data / . Now we create the file
/www/doodles/_build/data/transport.snippets.php in it. Put this code in it:
<?php function getSnippetContent($filename) { $o = file_get_contents($filename); $o = trim(str_replace(array('<?php','?>'),'',$o)); return $o; } $snippets = array(); $snippets[1]= $modx->newObject('modSnippet'); $snippets[1]->fromArray(array( 'id' => 1, 'name' => 'Doodles', 'description' => 'Displays a list of Doodles.', 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.doodles.php'), ),'',true,true); $properties = include $sources['data'].'properties/properties.doodles.php'; $snippets[1]->setProperties($properties); unset($properties); return $snippets;
First, we created a small helper method that will capture our pieces of code from files and remove the "<? Php" tags from it. Then we create a snippet object. Remember: do not need to save, just create. It's time to go back to the $ snippets array. Remember the commented part from the
build.transport.php file? Here is this part:
$modx->log(modX::LOG_LEVEL_INFO,'Packaging in snippets...'); $snippets = include $sources['data'].'transport.snippets.php'; if (empty($snippets)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in snippets.'); $category->addMany($snippets);
We remove commenting. Now our snippet is loaded into the vehicle category. Now add the properties we mentioned earlier.
Adding Snippet Properties
Create a file
/www/doodles/_build/data/properties/properties.doodles.php with the following content:
<?php $properties = array( array( 'name' => 'tpl', 'desc' => 'prop_doodles.tpl_desc', 'type' => 'textfield', 'options' => '', 'value' => 'rowTpl', 'lexicon' => 'doodles:properties', ), array( 'name' => 'sort', 'desc' => 'prop_doodles.sort_desc', 'type' => 'textfield', 'options' => '', 'value' => 'name', 'lexicon' => 'doodles:properties', ), array( 'name' => 'dir', 'desc' => 'prop_doodles.dir_desc', 'type' => 'list', 'options' => array( array('text' => 'prop_doodles.ascending','value' => 'ASC'), array('text' => 'prop_doodles.descending','value' => 'DESC'), ), 'value' => 'DESC', 'lexicon' => 'doodles:properties', ), ); return $properties;
This is the PHP representation of the default properties (parameters) of the snippet. Let's look at all his keys:
So, we have properties. But as you can see, we made a link to the new section of the lexicon "doodles: properties" Let's create a lexicon file
/www/doodles/core/components/doodles/lexicon/en/properties.inc.php with this content:
<?php $_lang['prop_doodles.ascending'] = 'Ascending'; $_lang['prop_doodles.descending'] = 'Descending'; $_lang['prop_doodles.dir_desc'] = 'The direction to sort by.'; $_lang['prop_doodles.sort_desc'] = 'The field to sort by.'; $_lang['prop_doodles.tpl_desc'] = 'The chunk for displaying each row.';
As you can see here the content is similar to the “default” section.
If you run the script now, then our category and snippet with its properties will be packaged in some sort. Fine! But we missed our add-on files themselves. Let's fix it.
Adding file resolvers (Resolvers)
Let's add folders
/ www / doodles / core / components / doodles / and
/ www / doodles / assets / components / doodles / of our add-on to the package. We will add files to our vehicle categories using so-called. file resolvers.
So, in
build.transport.php immediately after adding a vehicle category:
$vehicle = $builder->createVehicle($category,$attr);
add this:
$modx->log(modX::LOG_LEVEL_INFO,'Adding file resolvers to category...'); $vehicle->resolve('file',array( 'source' => $sources['source_assets'], 'target' => "return MODX_ASSETS_PATH . 'components/';", )); $vehicle->resolve('file',array( 'source' => $sources['source_core'], 'target' => "return MODX_CORE_PATH . 'components/';", ));
It is worth analyzing two attributes:
source is the way to find files. We use our source_assets and source_core, which were defined by us earlier.
target is an eval-string that returns the path where the files of our add-on will be located.
The first parameter in resolve () tells MODX that this is a file resolver. We will take a closer look at resolvers later in this lesson.
If you run the packer now, it will pack the
doodles / core / and
doodles / assets / folders.
Add menu item and action
Now let's add a menu item and an action for the component page that we made earlier.
Add this code:
$modx->log(modX::LOG_LEVEL_INFO,'Packaging in menu...'); $menu = include $sources['data'].'transport.menu.php'; if (empty($menu)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in menu.'); $vehicle= $builder->createVehicle($menu,array ( xPDOTransport::PRESERVE_KEYS => true, xPDOTransport::UPDATE_OBJECT => true, xPDOTransport::UNIQUE_KEY => 'text', xPDOTransport::RELATED_OBJECTS => true, xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array ( 'Action' => array ( xPDOTransport::PRESERVE_KEYS => false, xPDOTransport::UPDATE_OBJECT => true, xPDOTransport::UNIQUE_KEY => array ('namespace','controller'), ), ), )); $modx->log(modX::LOG_LEVEL_INFO,'Adding in PHP resolvers...'); $builder->putVehicle($vehicle); unset($vehicle,$menu);
Everything is similar to the vehicle category. A menu object and an associated action object are created.
- PRESERVE_KEYS is set to true, because The menus have unique keys and we want to keep the key of our menu item.
- The UNIQUE_KEY of the associated action object is an array. This tells MODX to look for a modAction object that has a namespace of 'namespace' => 'doodles' and a controller of controllers / index.
As you probably guessed, we need to add the file
transport.menu.php . Create it
/www/doodles/_build/data/transport.menu.php :
<?php $action= $modx->newObject('modAction'); $action->fromArray(array( 'id' => 1, 'namespace' => 'doodles', 'parent' => 0, 'controller' => 'controllers/index', 'haslayout' => true, 'lang_topics' => 'doodles:default', 'assets' => '', ),'',true,true); $menu= $modx->newObject('modMenu'); $menu->fromArray(array( 'text' => 'doodles', 'parent' => 'components', 'description' => 'doodles.desc', 'icon' => 'images/icons/plugin.gif', 'menuindex' => 0, 'params' => '', 'handler' => '', ),'',true,true); $menu->addOne($action); unset($menus); return $menu;
Everything is similar to
transport.snippets.php , except for calling the menu object's addOne () method. Please note that all the elements of the array fromArray () correspond to the fields in the database tables.
So, the menu item and action are packed.
Add resolver
When we install our add-on in the system, we will encounter one problem - the modx_doodles database tables will not exist. Let's write a PHP resolver that will run after a transport vehicle. Add this resolver to our vehicle menu. Immediately after
$ vehicle = $ builder-> createVehicle ($ menu), add the following code:
$modx->log(modX::LOG_LEVEL_INFO,'Adding in PHP resolvers...'); $vehicle->resolve('php',array( 'source' => $sources['resolvers'] . 'resolve.tables.php', ));
Create a file
/www/doodles/_build/resolvers/resolve.tables.php with the following content:
<?php if ($object->xpdo) { switch ($options[xPDOTransport::PACKAGE_ACTION]) { case xPDOTransport::ACTION_INSTALL: $modx =& $object->xpdo; $modelPath = $modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/'; $modx->addPackage('doodles',$modelPath); $manager = $modx->getManager(); $manager->createObjectContainer('Doodle'); break; case xPDOTransport::ACTION_UPGRADE: break; } } return true;
Fine. I think everything is clear. We have a switch construction, thanks to which we can perform tasks depending on the current action. We specify the path to our model and call the
addPackage () method, which adds our xpdo scheme (remember from the first lesson?). Finally, we run
$ modx-> getManager () and then
$ manager-> createObjectContainer ('Doodle') . This method gives the MODX command to run SQL and create a table in the database for our Doodle class. Now you can remove the check for the existence of a database table, as we did in the first part (using a resolver is not necessary, but it is convenient). And at the end we will return true, so that MODX knew that everything went smoothly.
Now when installing the package, a table of our addition to the database will be created.
Adding files cangelog, readme, license and installation parameters
Let's create a
readme.txt file in the
docs / folder with the following contents:
-------------------- Extra: Doodles -------------------- Version: 1.0 A simple demo extra for creating robust 3rd-Party Components in MODx Revolution.
Also, create the
license.txt files (contains a description of the license) and
changelog.txt (change log) if they are not already there.
Now let's go back to the
build.transport.php script and add the following lines in front of
$ builder-> pack () :
$modx->log(modX::LOG_LEVEL_INFO,'Adding package attributes and setup options...'); $builder->setPackageAttributes(array( 'license' => file_get_contents($sources['docs'] . 'license.txt'), 'readme' => file_get_contents($sources['docs'] . 'readme.txt'), 'changelog' => file_get_contents($sources['docs'] . 'changelog.txt'), 'setup-options' => array( 'source' => $sources['build'].'setup.options.php', ), ));
As you can see, the
setPackageAttributes () method is
called , which sets the attributes for our packer. Also here is a new for us array 'setup-options'. This array has an element with the key 'source' - the path to the PHP file (like a resolver).
Create a file
/www/doodles/_build/setup.options.php with the following content:
<?php $output = ''; switch ($options[xPDOTransport::PACKAGE_ACTION]) { case xPDOTransport::ACTION_INSTALL: $output = '<h2>Doodles Installer</h2> <p>Thanks for installing Doodles! Please review the setup options below before proceeding.</p><br />'; break; case xPDOTransport::ACTION_UPGRADE: case xPDOTransport::ACTION_UNINSTALL: break; } return $output;
Familiar looks, yes? This piece of code allows us to display the "Installation Options" when the user installs the package. Now we are just displaying a message to say “Thank you” to people for installing our add-on.
Here you can add form elements that will be displayed when the package is installed and further processed by the installer. An example can be seen in the Quip component:
github.com/splittingred/Quip/blob/develop/_build/resolvers/setupoptions.resolver.php .
That's all. Run the packer (http: //localhost/doodles/_build/build.transport.php) and the file
“doodles-1.0-rc1.zip” will appear in the
core / packages / folder. This file can be downloaded to
the MODX add-ons repository and then it can be installed via
Package Management .

All files created by us, the packer can be found here:
github.com/splittingred/Doodles/tree/develop/_build .