1. Getting Started
      1. Video Quick-Start Series
      2. Server Requirements
        1. MySQL 5.0.51 Issues
      3. Installation
        1. Basic Installation
          1. MODx Revolution on Debian
          2. Problems with WAMPServer 2.0i
          3. Lighttpd Guide
          4. Installation on a server running ModSecurity
          5. MODX and Suhosin
          6. Nginx Server Config
        2. Successful Installation, Now What Do I Do?
        3. Successful Installation, Now What Do I Do?
        4. Advanced Installation
        5. Git Installation
        6. Command Line Installation
          1. The Setup Config Xml File
        7. Troubleshooting Installation
        8. Using MODx Revolution from SVN
      4. An Overview of MODX
        1. Glossary of Revolution Terms
          1. Explanation of Directory Structure
        2. Roadmap
        3. MODX Revolution Framework Structure Ideology
        4. What's New in 2.3
    2. FAQs & Troubleshooting
      1. CMP Development FAQs & Troubleshooting
    3. Making Sites with MODX
      1. Structuring Your Site
        1. Resources
          1. Content Types
          2. Named Anchor
          3. Static Resource
          4. Symlink
            1. Using Resource Symlinks
          5. Weblink
        2. Templates
        3. Chunks
        4. Using Snippets
      2. Tag Syntax
      3. Customizing Content
        1. Template Variables
          1. Creating a Template Variable
          2. Adding a Custom TV Type - MODX 2.2
          3. Bindings
            1. CHUNK Binding
            2. DIRECTORY Binding
            3. EVAL Binding
            4. FILE Binding
            5. INHERIT Binding
            6. RESOURCE Binding
            7. SELECT Binding
          4. Template Variable Input Types
          5. Template Variable Output Types
            1. Date TV Output Type
            2. Delimiter TV Output Type
            3. HTML Tag TV Output Type
            4. Image TV Output Type
            5. URL TV Output Type
          6. Adding a Custom TV Input Type
          7. Adding a Custom TV Output Type
          8. Creating a multi-select box for related pages in your template
          9. Accessing Template Variable Values via the API
        2. Properties and Property Sets
        3. Input and Output Filters (Output Modifiers)
          1. Custom Output Filter Examples
      4. Commonly Used Template Tags
        1. Date Formats
    4. Administering Your Site
      1. Settings
        1. System Settings
          1. access_category_enabled
          2. date_timezone
          3. access_context_enabled
          4. access_resource_group_enabled
          5. allow_duplicate_alias
          6. allow_forward_across_contexts
          7. allow_multiple_emails
          8. allow_tags_in_post
          9. archive_with
          10. automatic_alias
          11. auto_check_pkg_updates
          12. auto_check_pkg_updates_cache_expire
          13. auto_menuindex
          14. base_help_url
          15. blocked_minutes
          16. cache_action_map
          17. cache_context_settings
          18. cache_db
          19. cache_db_expires
          20. cache_db_session
          21. cache_default
          22. cache_disabled
          23. cache_format
          24. cache_handler
          25. cache_json
          26. cache_json_expires
          27. cache_lang_js
          28. cache_lexicon_topics
          29. cache_noncore_lexicon_topics
          30. cache_resource
          31. cache_resource_expires
          32. cache_scripts
          33. cache_system_settings
          34. clear_cache_refresh_trees
          35. compress_css
          36. compress_js
          37. concat_js
          38. container_suffix
          39. cultureKey
          40. custom_resource_classes
          41. default_per_page
          42. default_template
          43. editor_css_path
          44. editor_css_selectors
          45. emailsender
          46. emailsubject
          47. enable_dragdrop
          48. error_page
          49. extension_packages
          50. failed_login_attempts
          51. feed_modx_news
          52. feed_modx_news_enabled
          53. feed_modx_security
          54. feed_modx_security_enabled
          55. fe_editor_lang
          56. filemanager_path
          57. filemanager_path_relative
          58. filemanager_url
          59. filemanager_url_relative
          60. forgot_login_email
          61. friendly_alias_lowercase_only
          62. forward_merge_excludes
          63. friendly_alias_max_length
          64. friendly_alias_restrict_chars
          65. friendly_alias_restrict_chars_pattern
          66. friendly_alias_strip_element_tags
          67. friendly_alias_translit
          68. friendly_alias_translit_class
          69. friendly_alias_translit_class_path
          70. friendly_alias_trim_chars
          71. friendly_alias_urls
          72. friendly_alias_word_delimiter
          73. friendly_alias_word_delimiters
          74. friendly_urls
          75. friendly_url_prefix
          76. friendly_url_suffix
          77. global_duplicate_uri_check
          78. hidemenu_default
          79. link_tag_scheme
          80. mail_charset
          81. mail_encoding
          82. mail_smtp_auth
          83. mail_smtp_helo
          84. mail_smtp_hosts
          85. mail_smtp_keepalive
          86. mail_smtp_pass
          87. mail_smtp_port
          88. mail_smtp_prefix
          89. mail_smtp_single_to
          90. mail_smtp_timeout
          91. mail_smtp_user
          92. mail_use_smtp
          93. manager_date_format
          94. manager_direction
          95. manager_favicon_url
          96. manager_language
          97. manager_lang_attribute
          98. manager_theme
          99. manager_time_format
          100. context_tree_sort
          101. context_tree_sortby
          102. context_tree_sortdir
          103. session_enabled
          104. upload_files
          105. modx_charset
          106. new_file_permissions
          107. new_folder_permissions
          108. password_generated_length
          109. password_min_length
          110. phpthumb_allow_src_above_docroot
          111. phpthumb_cache_maxage
          112. phpthumb_cache_maxfiles
          113. phpthumb_cache_maxsize
          114. phpthumb_cache_source_enabled
          115. phpthumb_document_root
          116. phpthumb_error_bgcolor
          117. phpthumb_error_fontsize
          118. phpthumb_error_textcolor
          119. phpthumb_far
          120. phpthumb_imagemagick_path
          121. phpthumb_nohotlink_enabled
          122. phpthumb_nohotlink_erase_image
          123. phpthumb_nohotlink_text_message
          124. phpthumb_nohotlink_valid_domains
          125. phpthumb_nooffsitelink_enabled
          126. phpthumb_nooffsitelink_erase_image
          127. phpthumb_nooffsitelink_require_refer
          128. phpthumb_nooffsitelink_text_message
          129. phpthumb_nooffsitelink_valid_domains
          130. phpthumb_nooffsitelink_watermark_src
          131. phpthumb_zoomcrop
          132. principal_targets
          133. proxy_auth_type
          134. proxy_host
          135. proxy_password
          136. proxy_port
          137. proxy_username
          138. publish_default
          139. rb_base_dir
          140. rb_base_url
          141. request_controller
          142. request_param_alias
          143. request_param_id
          144. resource_tree_node_name
          145. resource_tree_node_tooltip
          146. richtext_default
          147. search_default
          148. server_offset_time
          149. server_protocol
          150. session_cookie_domain
          151. session_cookie_lifetime
          152. session_cookie_path
          153. session_cookie_secure
          154. session_handler_class
          155. session_name
          156. settings_version
          157. signupemail_message
          158. site_name
          159. site_start
          160. site_status
          161. site_unavailable_message
          162. site_unavailable_page
          163. strip_image_paths
          164. symlink_merge_fields
          165. tree_default_sort
          166. tree_root_id
          167. tvs_below_content
          168. udperms_allowroot
          169. ui_debug_mode
          170. unauthorized_page
          171. upload_maxsize
          172. use_alias_path
          173. use_browser
          174. use_editor
          175. use_multibyte
          176. welcome_screen
          177. which_editor
          178. which_element_editor
          179. xhtml_urls
      2. Using Friendly URLs
      3. Contexts
        1. Creating a Subdomain from a Folder using Virtual Hosts
        2. Using One Gateway Plugin to Manage Multiple Domains
      4. Customizing the Manager
        1. Customizing the Manager via Plugins
        2. Form Customization Profiles
        3. Form Customization Sets
          1. Customizing Tabs via Form Customization
          2. MODX GitHub Contributor's Guide
        4. Manager Templates and Themes
      5. MODX GitHub Integrator's Guide
      6. Security
        1. Hardening MODX Revolution
        2. Policies
          1. ACLs
          2. Permissions
            1. Permissions - Administrator Policy
            2. Permissions - Resource Policy
          3. PolicyTemplates
        3. Resource Groups
        4. Roles
        5. Security Standards
        6. Security Tutorials
          1. More on the Anonymous User Group
          2. Creating a Second Super Admin User
          3. Giving a User Manager Access
          4. Making Member-Only Pages
          5. Restricting an Element from Users
        7. Troubleshooting Security
          1. Resetting a User Password Manually
        8. User Groups
        9. Users
      7. Installing a Package
        1. Troubleshooting Package Management
      8. Upgrading MODX
        1. Upgrading to Revolution 2.0.5
        2. Upgrading from 2.0.x to 2.1.x
        3. Upgrading from Versions Earlier than 2.0.5
        4. Upgrading to 2.2.x
        5. Upgrading to Revolution 2.0.0-rc-2
        6. Troubleshooting Upgrades
        7. Upgrading from MODx Evolution
          1. Functional Changes from Evolution
      9. Moving Your Site to a New Server, or to Root from Subfolder
      10. Media Sources
        1. Adding a Media Source
        2. Assigning Media Sources to TVs
        3. Media Source Types
          1. Media Source Type - File System
          2. Media Source Type - S3
        4. Securing a Media Source
          1. Creating a Media Source for Clients Tutorial
      11. Dashboards
        1. Assigning a Dashboard to a User Group
        2. Creating a Dashboard Widget
        3. Dashboard Widget Types
          1. Dashboard Widget Type - File
          2. Dashboard Widget Type - HTML
          3. Dashboard Widget Type - Inline PHP
          4. Dashboard Widget Type - Snippet
        4. Managing Your Dashboard
    5. Developing in MODX
      1. Code Standards
      2. Overview of MODX Development
        1. Developer Introduction
          1. Getting Started Developing
        2. Extras Directories
        3. Setting up a Development Environment
      3. Basic Development
        1. Static Elements
        2. Plugins
          1. System Events
            1. OnMODXInit
            2. OnFileManagerBeforeUpload
            3. OnFileManagerDirCreate
            4. OnFileManagerDirRemove
            5. OnFileManagerDirRename
            6. OnFileManagerFileCreate
            7. OnFileManagerFileRemove
            8. OnFileManagerFileRename
            9. OnFileManagerFileUpdate
            10. OnPackageInstall
            11. OnPackageUninstall
            12. OnPackageRemove
            13. OnBeforeCacheUpdate
            14. OnBeforeChunkFormDelete
            15. OnBeforeChunkFormSave
            16. OnBeforeDocFormDelete
            17. OnBeforeDocFormSave
            18. OnBeforeEmptyTrash
            19. OnBeforeManagerLogin
            20. OnBeforeManagerLogout
            21. OnBeforeManagerPageInit
            22. OnBeforePluginFormDelete
            23. OnBeforePluginFormSave
            24. OnBeforeSaveWebPageCache
            25. OnBeforeSnipFormDelete
            26. OnBeforeSnipFormSave
            27. OnBeforeTempFormDelete
            28. OnBeforeTempFormSave
            29. OnBeforeTVFormDelete
            30. OnBeforeTVFormSave
            31. OnBeforeUserActivate
            32. OnBeforeUserFormDelete
            33. OnBeforeUserFormSave
            34. OnBeforeWebLogin
            35. OnBeforeWebLogout
            36. OnCacheUpdate
            37. OnCategoryBeforeRemove
            38. OnCategoryBeforeSave
            39. OnCategoryRemove
            40. OnCategorySave
            41. OnChunkBeforeRemove
            42. OnChunkBeforeSave
            43. OnChunkFormDelete
            44. OnChunkFormPrerender
            45. OnChunkFormRender
            46. OnChunkFormSave
            47. OnChunkRemove
            48. OnChunkSave
            49. OnContextBeforeRemove
            50. OnContextBeforeSave
            51. OnContextFormPrerender
            52. OnContextFormRender
            53. OnContextRemove
            54. OnContextSave
            55. OnDocFormDelete
            56. OnDocFormPrerender
            57. OnDocFormRender
            58. OnDocFormSave
            59. OnDocPublished
            60. OnDocUnPublished
            61. OnEmptyTrash
            62. OnFileManagerUpload
            63. OnHandleRequest
            64. OnInitCulture
            65. OnLoadWebDocument
            66. OnLoadWebPageCache
            67. OnManagerAuthentication
            68. OnManagerLogin
            69. OnManagerLoginFormPrerender
            70. OnManagerLoginFormRender
            71. OnManagerLogout
            72. OnManagerPageAfterRender
            73. OnManagerPageBeforeRender
            74. OnManagerPageInit
            75. OnPageNotFound
            76. OnPageUnauthorized
            77. OnParseDocument
            78. OnPluginBeforeRemove
            79. OnPluginBeforeSave
            80. OnPluginEventRemove
            81. OnPluginFormDelete
            82. OnPluginFormPrerender
            83. OnPluginFormRender
            84. OnPluginFormSave
            85. OnPluginRemove
            86. OnPluginSave
            87. OnPropertySetBeforeRemove
            88. OnPropertySetBeforeSave
            89. OnPropertySetRemove
            90. OnPropertySetSave
            91. OnResourceGroupBeforeRemove
            92. OnResourceGroupBeforeSave
            93. OnResourceGroupRemove
            94. OnResourceGroupSave
            95. OnRichTextBrowserInit
            96. OnRichTextEditorInit
            97. OnRichTextEditorRegister
            98. OnSiteRefresh
            99. OnSiteSettingsRender
            100. OnTemplateVarBeforeRemove
            101. OnTemplateVarBeforeSave
            102. OnTemplateVarRemove
            103. OnTemplateVarSave
            104. OnUserActivate
            105. OnUserBeforeRemove
            106. OnUserBeforeSave
            107. OnUserChangePassword
            108. OnUserFormDelete
            109. OnUserFormSave
            110. OnUserNotFound
            111. OnUserRemove
            112. OnUserSave
            113. OnWebAuthentication
            114. OnWebLogin
            115. OnWebLogout
            116. OnWebPageComplete
            117. OnWebPageInit
            118. OnWebPagePrerender
        3. Snippets
          1. Adding CSS and JS to Your Pages Through Snippets
          2. How to Write a Good Chunk
          3. How to Write a Good Snippet
          4. Templating Your Snippets
        4. xPDO
      4. Advanced Development
        1. Caching
          1. Setting up Memcache in MODX
        2. Custom Manager Pages
          1. Custom Manager Pages in 2.3
          2. Actions and Menus
            1. Action List
          3. Custom Manager Pages Tutorial
          4. MODExt
            1. MODx.combo.ComboBox
            2. MODx.Console
            3. MODx.FormPanel
            4. MODx.grid.Grid
            5. MODx.grid.LocalGrid
            6. MODx.msg
            7. MODx.tree.Tree
            8. MODx.Window
            9. MODExt Tutorials
              1. 1. Ext JS Tutorial - Message Boxes
              2. 2. Ext JS Tutorial - Ajax Include
              3. 3. Ext JS Tutorial - Animation
              4. 4. Ext JS Tutorial - Manipulating Nodes
              5. 5. Ext JS Tutorial - Panels
              6. 7. Ext JS Tutoral - Advanced Grid
              7. 8. Ext JS Tutorial - Inside a CMP
            10. MODExt MODx Object
        3. Custom Resource Classes
          1. Creating a Resource Class
            1. Creating a Resource Class - Step 2
            2. Creating a Resource Class - Step 3
            3. Creating a Resource Class - Step 4
        4. Extending modUser
        5. From the Command Line (CLI)
        6. Internationalization
          1. Adding a Translation
        7. MODX Services
          1. modFileHandler
          2. modMail
          3. modRegistry
        8. Namespaces
        9. Package Management
          1. Package Dependencies
          2. Creating a 3rd Party Component Build Script
          3. Providers
          4. Transport Packages
        10. Using runProcessor
        11. Validating Requests: Tokens and Nonces
        12. Developing RESTful APIs
      5. Other Development Resources
        1. Summary of Legacy Code Removed in 2.1
        2. API Reference
        3. Class Reference
          1. modResource
            1. modResource.isMember
          2. modChunk
            1. modChunk.getContent
            2. modChunk.setContent
          3. modUser
            1. modUser.addSessionContext
            2. modUser.changePassword
            3. modUser.endSession
            4. modUser.getSessionContexts
            5. modUser.getSettings
            6. modUser.hasSessionContext
            7. modUser.isAuthenticated
            8. modUser.isMember
            9. modUser.loadAttributes
            10. modUser.removeSessionContext
            11. modUser.removeSessionContextVars
            12. modUser.removeSessionCookie
          4. modX
            1. modX.addEventListener
            2. modX.checkForLocks
            3. modX.checkSession
            4. modX.executeProcessor
            5. modX.getAuthenticatedUser
            6. modX.getCacheManager
            7. modX.getChildIds
            8. modX.getChunk
            9. modX.getConfig
            10. modX.getContext
            11. modX.getEventMap
            12. modX.getLoginUserID
            13. modX.getLoginUserName
            14. modX.getParentIds
            15. modX.getParser
            16. modX.getPlaceholder
            17. modX.getRegisteredClientScripts
            18. modX.getRegisteredClientStartupScripts
            19. modX.getRequest
            20. modX.getResponse
            21. modX.getService
            22. modX.getSessionState
            23. modX.getTree
            24. modX.getUser
            25. modX.getVersionData
            26. modX.handleRequest
            27. modX.hasPermission
            28. modX.initialize
            29. modX.invokeEvent
            30. modX.lexicon
            31. modX.makeUrl
            32. modX.parseChunk
            33. modX.regClientCSS
            34. modX.regClientHTMLBlock
            35. modX.regClientScript
            36. modX.regClientStartupHTMLBlock
            37. modX.regClientStartupScript
            38. modX.reloadConfig
            39. modX.removeAllEventListener
            40. modX.removeEventListener
            41. modX.runProcessor
            42. modX.runSnippet
            43. modX.sendError
            44. modX.sendErrorPage
            45. modX.sendForward
            46. modX.sendRedirect
            47. modX.sendUnauthorizedPage
            48. modX.setDebug
            49. modX.setPlaceholder
            50. modX.setPlaceholders
            51. modX.switchContext
            52. modX.toPlaceholder
            53. modX.toPlaceholders
            54. modX.unsetPlaceholder
            55. modX.unsetPlaceholders
        4. Loading MODX Externally
        5. Reserved Parameters
    6. Case Studies and Tutorials
      1. Developing an Extra in MODX Revolution
        1. Developing an Extra in MODX Revolution, Part II
        2. Developing an Extra in MODX Revolution, Part III
      2. Developing an Extra in MODX Revolution - MODX 2.1 and Earlier
        1. Developing an Extra in MODX Revolution, Part II - MODX 2.1 and Earlier
        2. Developing an Extra in MODX Revolution, Part III - MODX 2.1 and Earlier
      3. PHP Coding in MODx Revolution, Pt. I
        1. PHP Coding in MODx Revolution, Pt. II
        2. PHP Coding in MODx Revolution, Pt. III
      4. Using Custom Database Tables in your 3rd Party Components
      5. Creating a Blog in MODx Revolution
      6. Loading Pages in the Front-End via AJAX and jQuery Tabs
      7. Reverse Engineer xPDO Classes from Existing Database Table
      8. Integrating a Template into MODX Tutorial
      9. Quick and Easy MODX Tutorials
        1. Automated Server-Side Image Editing
      10. Adding Custom Fields to Manager Forms
      11. Create a Multilingual Website with migxMultiLang
      12. Managing Resources and Elements via SVN
    7. MODX Community Information
      1. Becoming a Core Contributor
      2. Filing Bug Reports
      3. Getting a MODx Account
      4. Using GitHub

Creating a 3rd Party Component Build Script

Last edited by Mark Hamstra on Feb 9, 2018.
Users using Revolution 2.0.0-beta-4 or earlier should note that the defines are different in beta5 and onward. An example: xPDOTransport::UNIQUE_KEYS in beta5+ is XPDO_TRANSPORT_UNIQUE_KEYS in beta4 and earlier. MODx recommends to just update to beta5/SVN.

A build script. What is that, you might ask? This is the meat of the packaging process; here is where your component is actually put into the nice, neat .zip transport package that you find on modxcms.com or through Revolution's Package Management section.

This tutorial will guide you through how to create one of those scripts. We'll be using a sample component called Quip, which contains a modAction, a few menus, some chunks and a snippet, lexicons, setup options, a license, a readme, and system settings. It's basically a quick, easy run through of all the basics to creating a fundamental build script.

Directory Structure

First off, let's take a quick look at our directory structure. This isn't always how you have to do it - this one is specifically built this way for SVN; but it's definitely recommended, especially with the assets/components/quip/ and core/components/quip/ structures, since that makes creating the transport package much easier.

Starting the Build Script

Create a new file. Typically, it's build.transport.php in the _build directory. Let's first start with some phpdoc comments at the top, and then start the timer.

 * Quip build script
 * @package quip
 * @subpackage build
$mtime = microtime();
$mtime = explode(" ", $mtime);
$mtime = $mtime[1] + $mtime[0];
$tstart = $mtime;
set_time_limit(0); /* makes sure our script doesnt timeout */

Now let's define some basic paths. We can define these up top into a "sources" array to make them easier to reach later in the build script. Note how the 'source_core' and 'source_assets' directories do not post-fix a foreslash onto their paths. This is required.

$root = dirname(dirname(__FILE__)).'/';
$sources= array (
    'root' => $root,
    'build' => $root .'_build/',
    'resolvers' => $root . '_build/resolvers/',
    'data' => $root . '_build/data/',
    'source_core' => $root.'core/components/quip',
    'lexicon' => $root . 'core/components/quip/lexicon/',
    'source_assets' => $root.'assets/components/quip',
    'docs' => $root.'core/components/quip/docs/',
unset($root); /* save memory */

Now, we'll need to include some files to get the build libraries we'll need. First, let's include a file we'll create called 'build.config.php' in our build dir.

require_once dirname(__FILE__) . '/build.config.php';

In this file, we'll want to define the location of our MODx Revolution installation so that the build script can know where to get the modX class, as well as where to put the package when finished. Our file will look somewhat like this:

 * Define the MODX path constants necessary for core installation
 * @package quip
 * @subpackage build
define('MODX_CORE_PATH', '/absolute/path/to/modx/core/');

You'll want to make sure to change the value of MODX_CORE_PATH to the absolute path of where your MODx Revolution core is installed. MODX_CONFIG_KEY can stay the same, unless you're doing a multi-domain install.

Now, you'll want to include the modX class, and instantiate it. We'll also initialize it into the 'mgr' context, and set the log output to HTML to make our errors and info messages nice and formatted - unless we're doing this from the cmd line, where we'll want just standard echo messages.

require_once MODX_CORE_PATH . 'model/modx/modx.class.php';

$modx= new modX();
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');

Okay, it's time for the meat. Let's first off use $modx->loadClass to load the modPackageBuilder class. Then we'll instantiate an instance of it, and create a package.

$modx->loadClass('transport.modPackageBuilder','',false, true);
$builder = new modPackageBuilder($modx);

The modPackageBuilder::createPackage function has 3 parameters:
name, version, and release. For us,
we'll be doing quip-0.1-alpha7, so let's go with that.

Next, we'll register a Namespace to this package. Not all packages need Namespaces; but all 3rd Party Components do. Basically, a Namespace is an organizing tool for MODx so that MODx can know what objects are tied to what package. This is helpful later on should we want to uninstall our package; we'd want it to remove the objects we'd install.

Plus, should we want to add any Lexicon Entries to this package (which we will), MODx does so by relating it to it's Namespace. Our package builder will assign our Lexicon Entries to the Namespace, so we can easily manage just our Lexicon Entries; not any others.

Packaging in Objects

Objects are packaged as Vehicles in MODx Revolution; basically think of a vehicle as a sort of storage system that transports the data and/or files into the zip package. Packages can contain many vehicles; vehicles can contain many objects or files - however, vehicles that contain an object must only have one reference object (or parent object, whichever you prefer) that the vehicle is based off of.

So, let's look at some examples for creating a vehicle before digging into our build script. This first example packages in a simple object, with some parameters:

$snippet = $modx->newObject('modSnippet');
$vehicle = $builder->createVehicle($snippet,array(
    xPDOTransport::UNIQUE_KEY => 'name',
    xPDOTransport::UPDATE_OBJECT => true,
    xPDOTransport::PRESERVE_KEYS => false,

So, first off, we created a snippet object. Note that you'll have to specify an arbitrary ID for it, even though we wont keep it later. This is required. Then, we used the 'createVehicle' function in modPackageBuilder to create the vehicle object. Let's look at those attributes options more closely:

  • xPDOTransport::UNIQUE_KEY (string/array) - Here you'd place the unique key that identifies the object you're creating. This will tell MODx to search for the modSnippet with the 'name' equal to the packaged in name (here, 'Test') when updating or removing the object. For most objects, this will be 'name'; others require different settings. Some might even require an array of two or more fields.
  • xPDOTransport::UPDATE_OBJECT (boolean) - Either true or false, this tells MODx whether or not to update the object if it is found in the DB upon install (or update). Sometimes, if the object is already there, you may not want to update it - the update might erase the user's current settings for that object.
  • xPDOTransport::PRESERVE_KEYS (boolean) - Either true or false, this tells MODx whether or not to rewrite the primary keys when the object is found. This can be useful if you're wanting the PKs to stay the same when you update - some PKs are auto_increment, and if you're wanting those to stay the same number, you'd set this to true. Note: If the object already exists, this feature only works if xPDOTransport::UPDATE_OBJECT is set to true as well. If the object is not found, it will work regardless.

Simple enough? So our example tells it to look for a Snippet named 'Test', and if it finds it, update its contents. If it doesnt find it, create it. However, if it does find it; we told MODx not to update its PK - there's no need to adjust that in this situation.

Now, what about related objects? What if I want to package in my modMenu, along with its Action associated with the modMenu? Here's a bit more complex scenario:

$action= $modx->newObject('modAction');
    'id' => 1,
    'namespace' => 'quip',
    'parent' => '0',
    'controller' => 'index',
    'haslayout' => '1',
    'lang_topics' => 'quip:default,file',
    'assets' => '',
$menu= $modx->newObject('modMenu');
    'text' => 'quip',
    'parent' => 'components',
    'description' => 'quip_desc',
    'icon' => 'images/icons/plugin.gif',
    'menuindex' => '0',
    'params' => '',
    'handler' => '',
$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'),

Okay, a bit more meat here. We're introducing 2 new parameters:

  • xPDOTransport::RELATED_OBJECTS (boolean) - Either true or false, this will tell MODx we want to search for related objects to this object. This must be set for the next parameter to work.
  • xPDOTransport::RELATED_OBJECT_ATTRIBUTES (array) - This defines the types and details of the related objects we want to grab. If you note, the format is simply an associative array of attributes - similar to the parent object's attributes - where the key is the "alias" of the related object we want to grab. The aliases can be found in the Schema, located in core/model/schema/modx.mysql.schema.xml.

So our example above tells us on the modAction (found by looking for the modAction with a namespace of 'quip' and a controller of 'index') to include the related modAction object that we package in. We packaged them in manually using xPDO's addOne function on the modAction.

Also, if we wanted to package in related objects to the modAction objects, we would just have had to define that in the 'Action' attributes and addMany (or addOne) on that action. You can go however deep in nesting that you want.

So, back to our script. To recap, so far we have:

 * Quip build script
 * @package quip
 * @subpackage build
$mtime = microtime();
$mtime = explode(" ", $mtime);
$mtime = $mtime[1] + $mtime[0];
$tstart = $mtime;

$root = dirname(dirname(__FILE__)).'/';
$sources= array (
    'root' => $root,
    'build' => $root .'_build/',
    'lexicon' => $root . '_build/lexicon/',
    'resolvers' => $root . '_build/resolvers/',
    'data' => $root . '_build/data/',
    'source_core' => $root.'core/components/quip',
    'source_assets' => $root.'assets/components/quip',
    'docs' => $root.'core/components/quip/docs/',

/* override with your own defines here (see build.config.sample.php) */
require_once dirname(__FILE__) . '/build.config.php';
require_once MODX_CORE_PATH . 'model/modx/modx.class.php';

$modx= new modX();
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');

$modx->loadClass('transport.modPackageBuilder','',false, true);
$builder = new modPackageBuilder($modx);

So, let's first package in our modActions and modMenus for our backend:

/* load action/menu */
$menu = include $sources['data'].'transport.menu.php';

$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'),
unset($vehicle,$action); /* to keep memory low */

Wait! Notice how I put the action data in a different file? You don't have to do this - it's completely personal preference - but it does keep our build script clean, and isolate our actions/menus to a separate file for easy management.

Let's do the same with our system settings:

/* load system settings */
$settings = include $sources['data'].'transport.settings.php';

$attributes= array(
    xPDOTransport::UNIQUE_KEY => 'key',
    xPDOTransport::PRESERVE_KEYS => true,
    xPDOTransport::UPDATE_OBJECT => false,
foreach ($settings as $setting) {
    $vehicle = $builder->createVehicle($setting,$attributes);

Great! We've got our actions, menus and settings packaged in. Now, using our newfound knowledge about related objects, let's create a category called 'Quip' and put our Snippet and Chunks in that category. We'll go through this a bit slower, so we can easily see how this works:

/* create category */
$category= $modx->newObject('modCategory');

Okay, great. Step one done: category created. Now about that Snippet:

/* create the snippet */
$snippet= $modx->newObject('modSnippet');
$snippet->set('name', 'Quip');
$snippet->set('description', 'A simple commenting component.');

Great! Note how here we're actually using the file_get_contents() function to grab the contents of the snippet from our dev environment and place it here. This makes it easy to run the build in future iterations; no need to continually update this call - just update that file.

Now, we had some properties on that snippet...how do we put those in?

$properties = include $sources['data'].'properties.inc.php';
We're using the addMany method here, and not the addOne method. Wether you need to use one or the other does not so much depend on the amount of objects you are relating (in this case only one snippet), but the cardinality of the relationship. That may sound complex - but the cardinality simply means if it is a one-on-one or one-to-many relationship. In this case, a category has a one-to-many relationship with snippets (there can be many snippets in one category) and that means you will have to use the addMany method. You can pass an array of objects or just one object to that method, but which one you use depends on the cardinality. Read more about relationships, addOne and addMany.

You'll use modSnippet's setProperties function to pass in an array of property arrays. So, let's take a look at that properties.inc.php file:

 * Default snippet properties
 * @package quip
 * @subpackage build
$properties = array(
        'name' => 'closed',
        'desc' => 'If set to true, the thread will not accept new comments.',
        'type' => 'combo-boolean',
        'options' => '',
        'value' => false,
        'name' => 'dateFormat',
        'desc' => 'The format of the dates displayed for a comment.',
        'type' => 'textfield',
        'options' => '',
        'value' => '%b %d, %Y at %I:%M %p',
    /* ...removed others for brevity... */
return $properties;

Simple enough. And now on to the chunks:

/* add chunks */
$chunks = include $sources['data'].'transport.chunks.php';
if (is_array($chunks)) {
} else { $modx->log(modX::LOG_LEVEL_FATAL,'Adding chunks failed.'); }

Good. We returned an array of chunks, and used modCategory's addMany() function to add them in. We also added a sanity check just in case we made a typo or something. Now, let's package all that into a vehicle:

/* create category vehicle */
$attr = array(
    xPDOTransport::UNIQUE_KEY => 'category',
    xPDOTransport::PRESERVE_KEYS => false,
    xPDOTransport::UPDATE_OBJECT => true,
    xPDOTransport::RELATED_OBJECTS => true,
    xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array (
        'Snippets' => array(
            xPDOTransport::PRESERVE_KEYS => false,
            xPDOTransport::UPDATE_OBJECT => true,
            xPDOTransport::UNIQUE_KEY => 'name',
        'Chunks' => array (
            xPDOTransport::PRESERVE_KEYS => false,
            xPDOTransport::UPDATE_OBJECT => true,
            xPDOTransport::UNIQUE_KEY => 'name',
$vehicle = $builder->createVehicle($category,$attr);

Great! We've got our category vehicle, complete with all the related chunks and snippet. They'll be installed in the right category when our users install our package, too - so it'll look nice and sharp!

Validators and Resolvers

Validators and resolvers are basically scripts that run during the install process. Validators are run pre-install; meaning that they are run before the main package installation happens. If they return false, the installation does not proceed.

Resolvers, on the other hand, execute after the main package has installed. They can either be file or PHP scripts. A file resolver simply copies over files into a specific target location. A PHP resolver executes a script after install.

With that said, we're going to attach 2 file resolvers, and one PHP resolver, to our script:

    'source' => $sources['source_core'],
    'target' => "return MODX_CORE_PATH . 'components/';",
    'source' => $sources['source_assets'],
    'target' => "return MODX_ASSETS_PATH . 'components/';",
    'source' => $sources['resolvers'] . 'setupoptions.resolver.php',

Okay, first things first. File resolvers take two options:

  • source - This is the target directory or script. If it's a file resolver, it must not end with a trailing slash and must be a valid directory. If it's a PHP script resolver, it must be a valid and accessible file.
  • target - Only applicable to file resolvers, this tells MODx where to install the source files. It is an eval()'ed statement, so must be used as in the example. The standard MODx defines are available to you; use those to grab base paths to target.

So in our examples, we simply move all the files in our source core directory to modx/core/components/quip/ (since our directory that we're moving is named "quip"), and all the files in our source assets directory to modx/assets/components/quip/.

You might be asking why we're moving these to two directories. Well, in practice, it's best to keep non-web-accessible files - such as PHP scripts, tpl files, docs, etc - in the core (which can be placed outside the webroot) so that they are kept secure from web visitors. This keeps only the files that need to be accessed through the web by your Component in the web-accessible part of your site.

Next, we add a PHP resolver, called 'setupoptions.resolver.php'. We'll get back to this in much more detail, because it actually deals with the setup options process we'll get to later.

And finally, we pack the vehicle into the package using the putVehicle function.


So now we've got a package with system settings, actions, menus, snippets, chunks, a category, and a few resolvers all set up. Let's talk about our lexicons.

We have our lexicon structured nicely in our \core/components/quip/lexicon directory:

As you can see, we have a subdirectory as 'en', the IANA code for English. Then, we have a 'default.inc.php' - this represents the 'default' lexicon topic. Should we want to create separate lexicon topics, we would name them 'topicname.inc.php'.

As of MODx Revolution RC-2, MODx will automatically find the lexicons in your lexicon directory, assuming that you put them in this structure in the following place: '{namespace_path}lexicon/', where the Namespace path is the path you put for your Namespace earlier. You don't have to build in the lexicons directly at all; MODx will parse it for you.

This is because the lexicons are cached first from your files, then any overrides from the DB are merged and cached. This allows people to 'override' your lexicons by using Lexicon Management in the Manager, should they choose to, without breaking their upgrade path for your Component.

Package Attributes: License, Readme and Setup Options

Each package has what are called 'package attributes', which can be passed to any resolver or validator. You could pass pretty much anything you want into the function modPackageBuilder::setPackageAttributes(), in an array format. There are, however, three special keys that we'll deal with.

  • license (string) - This represents your license agreement. Should MODx find this not empty during install, it will prompt the user to agree to it before they can proceed to install the package.
  • readme (string) - This holds the readme. Before installing, if this is not empty, the user will be able to view the readme. This can be useful to make sure people see any requirements before they install.
  • setup-options (string) - And here is the best part - this can be an HTML form (minus the form tags) that will pass any user-inputted options to the resolvers or validators. This means that you can take in user input before install, and process it during install!

So let's use these in our build script:

/* now pack in the license file, readme and setup options */
    'license' => file_get_contents($sources['docs'] . 'license.txt'),
    'readme' => file_get_contents($sources['docs'] . 'readme.txt'),
    'setup-options' => array(
        'source' => $sources['build'] . 'setup.options.php'

Obviously our license and readme values are being passed the contents of our license and readme files. We're doing them via file_get_contents() so that we can still store the actual files in the modx/core/components/quip/docs directory after install, should the user want to view them later.

But 'setup-options' looks a little different. We could just pass a file_get_contents() call that puts in a string, but then our setup options form wouldn't be dynamic! There might be cases where you wouldn't want that, but we do. We want this options form to upgrade well. Note that you have to pass the file location as the 'source' parameter - remember Resolvers? Looks familiar, eh? Same idea.

Our setup.options.php file looks like this:

 * Build the setup options form.
 * @package quip
 * @subpackage build
/* set some default values */
$values = array(
    'emailsTo' => 'my@emailhere.com',
    'emailsFrom' => 'my@emailhere.com',
    'emailsReplyTo' => 'my@emailhere.com',
switch ($options[xPDOTransport::PACKAGE_ACTION]) {
    case xPDOTransport::ACTION_INSTALL:
    case xPDOTransport::ACTION_UPGRADE:
        $setting = $modx->getObject('modSystemSetting',array('key' => 'quip.emailsTo'));
        if ($setting != null) { $values['emailsTo'] = $setting->get('value'); }

        $setting = $modx->getObject('modSystemSetting',array('key' => 'quip.emailsFrom'));
        if ($setting != null) { $values['emailsFrom'] = $setting->get('value'); }

        $setting = $modx->getObject('modSystemSetting',array('key' => 'quip.emailsReplyTo'));
        if ($setting != null) { $values['emailsReplyTo'] = $setting->get('value'); }
    case xPDOTransport::ACTION_UNINSTALL: break;

$output = '<label for="quip-emailsTo">Emails To:</label>
<input type="text" name="emailsTo" id="quip-emailsTo" width="300" value="'.$values['emailsTo'].'" />
<br /><br />

<label for="quip-emailsFrom">Emails From:</label>
<input type="text" name="emailsFrom" id="quip-emailsFrom" width="300" value="'.$values['emailsFrom'].'" />
<br /><br />

<label for="quip-emailsReplyTo">Emails Reply-To:</label>
<input type="text" name="emailsReplyTo" id="quip-emailsReplyTo" width="300" value="'.$values['emailsReplyTo'].'" />';

return $output;

As you can see, some new constants here. These are available to all setup options forms and resolvers:

  • xPDOTransport::PACKAGE_ACTION - This tells us what action is being performed on the package; it is one of the following 3 values:
    • xPDOTransport::ACTION_INSTALL - This is set when the package is being executed as an install.
    • xPDOTransport::ACTION_UPGRADE - This is set when the package is being upgraded.
    • xPDOTransport::ACTION_UNINSTALL - This is set when the package is being uninstalled. This doesn't apply to setup-options, obviously, since nothing is being set up. In future Revolution releases, it will allow you to do specific options for uninstall; but not yet.

Basically, we're presenting them with a form before install that looks like this:

So that they can set or update the values of the emailsTo, emailsFrom, and emailsReplyTo system settings before they install the package. Now, the script will first check to see if those settings already exist; and if so, we'll fill them in with those values. This allows for upgrades to go gracefully, persisting the user's custom settings for those values. Pretty cool, huh?

Obviously, there's a lot you could do with this. You could set target directories for photo locations, setup basic email accounts, set login/pass information for 3rd party web service integrations, and more. We'll leave your imagination to do the work from here on out.

Let's go back to our PHP script resolver that processes this information:

 * Resolves setup-options settings by setting email options.
 * @package quip
 * @subpackage build
$success= false;
switch ($options[xPDOTransport::PACKAGE_ACTION]) {
    case xPDOTransport::ACTION_INSTALL:
    case xPDOTransport::ACTION_UPGRADE:
        /* emailsTo */
        $setting = $object->xpdo->getObject('modSystemSetting',array('key' => 'quip.emailsTo'));
        if ($setting != null) {
        } else {
            $object->xpdo->log(xPDO::LOG_LEVEL_ERROR,'[Quip] emailsTo setting could not be found, so the setting could not be changed.');

        /* emailsFrom */
        $setting = $object->xpdo->getObject('modSystemSetting',array('key' => 'quip.emailsFrom'));
        if ($setting != null) {
        } else {
            $object->xpdo->log(xPDO::LOG_LEVEL_ERROR,'[Quip] emailsFrom setting could not be found, so the setting could not be changed.');

        /* emailsReplyTo */
        $setting = $object->xpdo->getObject('modSystemSetting',array('key' => 'quip.emailsReplyTo'));
        if ($setting != null) {
        } else {
            $object->xpdo->log(xPDO::LOG_LEVEL_ERROR,'[Quip] emailsReplyTo setting could not be found, so the setting could not be changed.');

        $success= true;
    case xPDOTransport::ACTION_UNINSTALL:
        $success= true;
return $success;

Note that $modx is not available here; you're actually running these scripts from within the transport object. The $modx object is available as a different name, however: $object->xpdo. $object is the object that the resolver is attached to; here, it would be the modCategory.

Our script, then, is setting the values set in the setup-options to the newly installed system settings.

And now that we've got everything packaged and ready to go, let's pack the package into a zip file and give us the time it took to build the package:


$mtime= microtime();
$mtime= explode(" ", $mtime);
$mtime= $mtime[1] + $mtime[0];
$tend= $mtime;
$totalTime= ($tend - $tstart);
$totalTime= sprintf("%2.4f s", $totalTime);

$modx->log(modX::LOG_LEVEL_INFO,"\nPackage Built.\nExecution time: {$totalTime}\n");


Great, we're done! You'll only need to run this script now, and viola! A fully zipped transport package file will appear in your core/packages directory.

Related Pages

Suggest an edit to this page on GitHub (Requires GitHub account. Opens a new window/tab) or become an editor of the MODX Documentation.