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. modx_charset
          105. new_file_permissions
          106. new_folder_permissions
          107. password_generated_length
          108. password_min_length
          109. phpthumb_allow_src_above_docroot
          110. phpthumb_cache_maxage
          111. phpthumb_cache_maxfiles
          112. phpthumb_cache_maxsize
          113. phpthumb_cache_source_enabled
          114. phpthumb_document_root
          115. phpthumb_error_bgcolor
          116. phpthumb_error_fontsize
          117. phpthumb_error_textcolor
          118. phpthumb_far
          119. phpthumb_imagemagick_path
          120. phpthumb_nohotlink_enabled
          121. phpthumb_nohotlink_erase_image
          122. phpthumb_nohotlink_text_message
          123. phpthumb_nohotlink_valid_domains
          124. phpthumb_nooffsitelink_enabled
          125. phpthumb_nooffsitelink_erase_image
          126. phpthumb_nooffsitelink_require_refer
          127. phpthumb_nooffsitelink_text_message
          128. phpthumb_nooffsitelink_valid_domains
          129. phpthumb_nooffsitelink_watermark_src
          130. phpthumb_zoomcrop
          131. principal_targets
          132. proxy_auth_type
          133. proxy_host
          134. proxy_password
          135. proxy_port
          136. proxy_username
          137. publish_default
          138. rb_base_dir
          139. rb_base_url
          140. request_controller
          141. request_param_alias
          142. request_param_id
          143. resource_tree_node_name
          144. resource_tree_node_tooltip
          145. richtext_default
          146. search_default
          147. server_offset_time
          148. server_protocol
          149. session_cookie_domain
          150. session_cookie_lifetime
          151. session_cookie_path
          152. session_cookie_secure
          153. session_handler_class
          154. session_name
          155. settings_version
          156. signupemail_message
          157. site_name
          158. site_start
          159. site_status
          160. site_unavailable_message
          161. site_unavailable_page
          162. strip_image_paths
          163. symlink_merge_fields
          164. tree_default_sort
          165. tree_root_id
          166. tvs_below_content
          167. udperms_allowroot
          168. ui_debug_mode
          169. unauthorized_page
          170. upload_maxsize
          171. use_alias_path
          172. use_browser
          173. use_editor
          174. use_multibyte
          175. welcome_screen
          176. which_editor
          177. which_element_editor
          178. 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. Plugins
          1. System Events
            1. OnBeforeCacheUpdate
            2. OnBeforeChunkFormDelete
            3. OnBeforeChunkFormSave
            4. OnBeforeDocFormDelete
            5. OnBeforeDocFormSave
            6. OnBeforeEmptyTrash
            7. OnBeforeManagerLogin
            8. OnBeforeManagerLogout
            9. OnBeforeManagerPageInit
            10. OnBeforePluginFormDelete
            11. OnBeforePluginFormSave
            12. OnBeforeSaveWebPageCache
            13. OnBeforeSnipFormDelete
            14. OnBeforeSnipFormSave
            15. OnBeforeTempFormDelete
            16. OnBeforeTempFormSave
            17. OnBeforeTVFormDelete
            18. OnBeforeTVFormSave
            19. OnBeforeUserActivate
            20. OnBeforeUserFormDelete
            21. OnBeforeUserFormSave
            22. OnBeforeWebLogin
            23. OnBeforeWebLogout
            24. OnCacheUpdate
            25. OnCategoryBeforeRemove
            26. OnCategoryBeforeSave
            27. OnCategoryRemove
            28. OnCategorySave
            29. OnChunkBeforeRemove
            30. OnChunkBeforeSave
            31. OnChunkFormDelete
            32. OnChunkFormPrerender
            33. OnChunkFormRender
            34. OnChunkFormSave
            35. OnChunkRemove
            36. OnChunkSave
            37. OnContextBeforeRemove
            38. OnContextBeforeSave
            39. OnContextFormPrerender
            40. OnContextFormRender
            41. OnContextRemove
            42. OnContextSave
            43. OnDocFormDelete
            44. OnDocFormPrerender
            45. OnDocFormRender
            46. OnDocFormSave
            47. OnDocPublished
            48. OnDocUnPublished
            49. OnEmptyTrash
            50. OnFileManagerUpload
            51. OnHandleRequest
            52. OnInitCulture
            53. OnLoadWebDocument
            54. OnLoadWebPageCache
            55. OnManagerAuthentication
            56. OnManagerLogin
            57. OnManagerLoginFormPrerender
            58. OnManagerLoginFormRender
            59. OnManagerLogout
            60. OnManagerPageAfterRender
            61. OnManagerPageBeforeRender
            62. OnManagerPageInit
            63. OnPageNotFound
            64. OnPageUnauthorized
            65. OnParseDocument
            66. OnPluginBeforeRemove
            67. OnPluginBeforeSave
            68. OnPluginEventRemove
            69. OnPluginFormDelete
            70. OnPluginFormPrerender
            71. OnPluginFormRender
            72. OnPluginFormSave
            73. OnPluginRemove
            74. OnPluginSave
            75. OnPropertySetBeforeRemove
            76. OnPropertySetBeforeSave
            77. OnPropertySetRemove
            78. OnPropertySetSave
            79. OnResourceGroupBeforeRemove
            80. OnResourceGroupBeforeSave
            81. OnResourceGroupRemove
            82. OnResourceGroupSave
            83. OnRichTextBrowserInit
            84. OnRichTextEditorInit
            85. OnRichTextEditorRegister
            86. OnSiteRefresh
            87. OnSiteSettingsRender
            88. OnTemplateVarBeforeRemove
            89. OnTemplateVarBeforeSave
            90. OnTemplateVarRemove
            91. OnTemplateVarSave
            92. OnUserActivate
            93. OnUserBeforeRemove
            94. OnUserBeforeSave
            95. OnUserChangePassword
            96. OnUserFormDelete
            97. OnUserFormSave
            98. OnUserNotFound
            99. OnUserRemove
            100. OnUserSave
            101. OnWebAuthentication
            102. OnWebLogin
            103. OnWebLogout
            104. OnWebPageComplete
            105. OnWebPageInit
            106. OnWebPagePrerender
        2. 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
        3. 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. Creating a 3rd Party Component Build Script
          2. Providers
          3. 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

Developing an Extra in MODX Revolution, Part III - MODX 2.1 and Earlier

This tutorial is part of a Series:

Overview

In this tutorial, we're going to be packaging up our Extra that we made in the past two tutorials into a Transport Package (TP) so that we can install it on any MODX installation, and even submit it to the Extras section on modx.com.

First off, if you're not sure what a Transport Package is, or what Package Management is, I suggest you read up on Package Management and Transport Packages first before proceeding.

Our main goals for this part of the tutorial will be getting the Extra in a package: specifically our Snippet; the files in core/components and assets/components; the Action, Menu and Namespace for our CMP; making our Snippet's default properties i18n supported; and finally, adding a Resolver that creates our custom DB table in the installing user's database.

For simple Extras that don't have a CMP, we could just use PackMan to package up our Extra. However, we have a CMP, and we want to learn how to do the script. So, here we are.

Setting Up Our Build Directory

This is what our _build directory will look like when we're through. We're already familiar with the build.config.php and build.schema.php files from Part I. For now, let's just explain a few things about it:

  • data - This is where we're going to put all of our data-packaging scripts. We'll get into that shortly.
  • resolvers - A directory containing our resolvers for our Transport Package.
  • build.transport.php - This is our main build script. Running it will create the Transport Package. We'll be heavily looking at this file.
  • setup.options.php - While we won't use this extensively for our TP, we'll look at it briefly to show what's possible with it.

The Build Script

Let's go ahead and create a file at /www/doodles/_build/build.transport.php, and fill it with this:

$tstart = explode(' ', microtime());
$tstart = $tstart[1] + $tstart[0];
set_time_limit(0);

/* define package names */
define('PKG_NAME','Doodles');
define('PKG_NAME_LOWER','doodles');
define('PKG_VERSION','1.0');
define('PKG_RELEASE','beta4');

/* define build paths */
$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);

/* override with your own defines here (see build.config.sample.php) */
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>'; /* used for nice formatting of log messages */
$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.'/');

/* zip up package */
$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's a lot in there. It's helpful to note that all this is doing so far is packaging in our Namespace, and creating a "doodles-1.0-beta4.zip" transport file. Let's go deeper to understand a bit more of it.

$tstart = explode(' ', microtime());
$tstart = $tstart[1] + $tstart[0];
set_time_limit(0);

/* define package names */
define('PKG_NAME','Doodles');
define('PKG_NAME_LOWER','doodles');
define('PKG_VERSION','1.0');
define('PKG_RELEASE','beta4');

First off, we're going to get the time started for this build script so we can output at the end how long it took to build it. It's definitely not necessary code to build the TP, but it's useful information anyway.

Then we'll set up some defines we'll use later to determine our package's name, version and release. Next:

/* define build paths */
$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);

/* override with your own defines here (see build.config.sample.php) */
require_once $sources['build'] . 'build.config.php';
require_once MODX_CORE_PATH . 'model/modx/modx.class.php';

Okay, here we're defining a whole bunch of paths on where to find stuff in our directory structure. This will be useful later on in our build script, so we can easily reference locations of files.

Note the source_core and source_assets keys - it's very important to note that they do not have a trailing slash. When we package them in later, this is important.

Finally, we'll include our build.config.php file and our modx class. Now it's time to load up the modX object:

$modx= new modX();
$modx->initialize('mgr');
echo '<pre>'; /* used for nice formatting of log messages */
$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.'/');

Okay, a bit in here. First off, we'll instantiate the modX object, and initialize our 'mgr' Context. This sets up all the modX environment stuff we'll need. Next, we'll tell MODX to be a little more verbose in it's error reporting during this build script with the $modx->setLogLevel method - and we'll tell it to output to the screen as well with the setLogTarget message.

Then we'll load the 'modPackageBuilder' class from the transport/ directory in core/model/modx/ (which is the default since we passed '' into the 3rd parameter of loadClass), which is an assistance class that we'll use to package up our Extra.

Then we get into 2 interesting methods: createPackage and registerNamespace.

$modx->createPackage(key,version,release)

Here's where the name for our TP gets created. We'll want to pass the name of our Extra (in lower case with no dots or hyphens!) in the first parameter. Then we'll want to pass a version and a release number. We chose '1.0' and 'beta4' back when we did our defines (remember that?). Now, modTransportPackage has an assistance method to automatically package in our Namespace for us:

$builder->registerNamespace(namespace_name,autoincludes,packageNamespace,namespacePath)

The first parameter is the name of our Namespace ('doodles' for us). The 2nd parameter auto-packages in an array of classes associated with our Namespace (we don't want this, so we set it to false). The third parameter asks if we want to add the Namespace to the TP (we do, so we set it to true). And finally, the last parameter asks what the path of our Namespace should eventually be.

That last parameter is key - note how we make it resolve to: '{core_path}components/doodles/'? The {core_path} part is a placeholder that will get replaced by MODX when the Namespace is accessed in their installation. This makes our Package nice and flexible on its paths - we don't have to explicitly set them, and it becomes more easy to 'transport', so to speak.

And now finally, the last few lines in our script:

/* zip up package */
$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 go ahead and make the Transport Package zip with our built package so far. The rest of the lines after that just display how long it took to do the build. That's it! If you run this via the browser (on mine, http://localhost/doodles/_build/build.transport.php) you'll get some debugging info displayed, and then in your MODX's core/packages/ directory, you'll find this:

Our Transport Package! Nice and done. However, installing it wont actually do anything. Let's try and solve that.

Adding in the Data

We're going to want to package in our Snippet in its own 'Doodles' category, to get it to be nice and separated out from other Snippets the user might be using. In our build.transport.php file, add this below our registerNamespace call:

$category= $modx->newObject('modCategory');
$category->set('id',1);
$category->set('category',PKG_NAME);

/* add snippets */
//$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);

/* 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',
        ),
    ),
);
$vehicle = $builder->createVehicle($category,$attr);
$builder->putVehicle($vehicle);

Quite a bit of this is detailed in this tutorial here, but we'll go over it again here. First off, we create a modCategory object that has the name (category) of 'Doodles'. Great. Note how we don't ->save() on it - we just want the object. Next we have some code to package in the Snippet, but we've commented it out for now. Go ahead and ignore it - we'll come back to it.

Next, we create this really big array of attributes, it seems. A bit more on these - they are attributes for the Vehicle for the Category. What's a Vehicle? Well, a Vehicle "carries" an Object in the Transport Package. Each object (say, a Snippet, Menu, Category, etc) needs a Vehicle to be carried in the Transport Package. So we'll create one, but first we want to assign some attributes to it to tell MODX just how this Vehicle should behave when the user installs it.

  • xPDOTransport::UNIQUE_KEY => 'category' - Here, we're telling MODX that the unique key for this Category is the field 'category'. Since we are installing this on another machine, the ID of the Category there will most likely be different than our ID on our machine. So MODX needs some way of identifying our 'Doodles' category if the User were to decide and uninstall our Doodles Extra. MODX uses this UNIQUE_KEY property to look for a modCategory object with 'category' => "Doodles", and then removes it there.
  • xPDOTransport::PRESERVE_KEYS => false - Sometimes, however, we want the primary key of our object to be 'preserved' - or rather, used when the User installs our package. This is useful for non-auto-incrementing primary keys (PKs), such as Menu items, which we'll get to later. Our Category doesn't need its ID preserved, so we'll set that to false here.
  • xPDOTransport::UPDATE_OBJECT => true - A crucial one. This tells MODX that if the Category already exists, update it with our version. If we had set this to false, MODX would just skip this Category if it found it. We don't want that - say we want to release an update for our Doodles Extra later; we'd want the Category to update.
  • xPDOTransport::RELATED_OBJECTS => true - This tells MODX that we have some related objects to our Category we want to package in. (We do. We have a Snippet.) Related objects are important, as this means that they will be "related" to one another on install. Our example is a good one - any Snippets we install, we want to assign to the Category we're installing.
  • xPDOTransport::RELATED_OBJECT_ATTRIBUTES - This takes in an associative array. Each index in the first depth of it tells MODX what the alias of it is - note we only have one, "Snippets". That tells MODX to look for any Related Objects in this Category that are Snippets, and then below that defines properties for those Snippets:
'Snippets' => array(
   xPDOTransport::PRESERVE_KEYS => false,
   xPDOTransport::UPDATE_OBJECT => true,
   xPDOTransport::UNIQUE_KEY => 'name',
),

We're going to tell the package to not preserve the Snippet's keys (similarly to the Category). Then we want to update it should MODX find it already during installs or upgrades. Finally, we tell MODX that it's unique key is 'name' - MODX will look for a Snippet with the name of 'Doodles' (we'll get to where that's defined here in a bit) during install, and if it finds it, upgrade it (or remove it during uninstall).

Then we hit this:

$vehicle = $builder->createVehicle($category,$attr);
$builder->putVehicle($vehicle);

This packages our Category object into a nice little vehicle for us, with the attributes we just defined. And then it adds it to the Transport Package. Done! Our Category is now in the TP. But we need to add the Snippets to it!

Adding the Snippet

Go ahead and create a directory at /www/doodles/_build/data/. Now let's add a file at /www/doodles/_build/data/transport.snippets.php. Place this in there:

<?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 off, we're going to make a little helper method that grabs our snippet we worked on earlier and strips the <?php tags from it. Then, we'll make a $snippets array - basically an array of all the Snippets we want to package up.

Next, we'll actually make a new Snippet object. Note, however, we're not saving it - just creating it. Also, we're going to include some properties on them (more on that in a second). Finally, we return the $snippets array. Remember that part we commented out in our build.transport.php file? This part:

/* add snippets */
$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);

Go ahead and comment it out. Now our Snippets are loaded into the Category Vehicle. We're done there. Let's add those properties that we mentioned earlier.

Adding in Snippet Properties

Create a file at /www/doodles/_build/data/properties/properties.doodles.php. Put this in it:

<?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;

These are PHP representations of the default Properties for our Snippet. Let's look at the keys they have:

  • name - This is the name, or key, of the property. We've got tpl, sort, and dir. For example, in our tpl property, we're telling it to default to 'rowTpl'. When someone wants to use the property, it would look like this in their snippet call:

[[Doodles? &tpl=`rowTpl`]]

  • desc - The description of our property. This can either be the actual description, or, if the 'lexicon' attribute on this property is set, a Lexicon key. We've got it as a Lexicon key, because we're going to i18n our properties.
  • type - This is the 'xtype' of the property. Currently, the 4 available values are "textfield", "textarea", "combo-boolean" (Yes/No) and "list". We've got two textfields here, and a list type.
  • options - Only used by the 'list' type, this is an array of arrays, which each contain an option in the list. Each option has two values - 'text' and 'value', where value is the actual value stored when it's selected, and 'text' is the text displayed for the value. 'text' can be a Lexicon key, if wanted.
  • value - The default value of the property.
  • lexicon - If wanted, properties can be i18n-compatible. Just specify the Lexicon Topic here, and MODX will handle the rest.

Okay, so we've got our properties. But as you can see, we've referenced a 'doodles:properties' Lexicon Topic. Let's go ahead and create that, in the file /www/doodles/core/components/doodles/lexicon/en/properties.inc.php:

<?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, it's a similar format to our default topic. Also, the keys in each string here match with the 'desc' attribute in each of our properties. This means that our Snippet's properties will be translated when they are shown - useful for making Extras that are translatable!

If you run the build script now, your Category, Snippet and its Properties will be packaged in. Great! But we've missed something - the actual files aren't getting copied. Let's remedy that.

Adding the File Resolvers

So we want to add /www/doodles/core/components/doodles/ and /www/doodles/assets/components/doodles/ to our Transport Package. We're going to add those files to our Category Vehicle, via what are called File Resolvers. These scripts run after the package has been installed, and can be used to copy files into the User's MODX installation.

So, in build.transport.php, right after this, where we add the Category Vehicle:

$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/';",
));

Let's explain. First off, there are two attributes here worth noting:

  • source - This is the source of the files, or the path in which they can be found. This points to our source_assets and source_core paths, which were defined above. Note the lack of a trailing slash, as we mentioned earlier.
  • target - This an eval'ed statement that returns where the script will be installed. Here, we're telling it to install to the assets path and core path of the User's MODX install, respectively.

Also, the first parameter of the resolve() call tells MODX this is a 'file' resolver. We'll be looking into PHP resolvers later on in this tutorial.

If you run the build script now, it will package in your doodles/core/ and doodles/assets/ directories, and install them into the User's proper directories. Great!

Adding the Menu and Action

Now that we've got most of our Extra nice and packaged, let's add in the Menu and Action that make up our Custom Manager Page (CMP). Add this code below the putVehicle line for our Category:

$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);

Very similar to our Category Vehicle creation code. We've also got a related object of our Action. There are a couple differences, however, worth noting:

  • PRESERVE_KEYS is set to 'true' on our menu. This is because menu keys are unique - and we want to preserve that for our installed menu.
  • UNIQUE_KEY of the related object Action is an array. This tells MODX to look for a modAction object that has both a 'namespace' => 'doodles' and a controller of 'controllers/index'. It's a bit more specific on the search.

As you probably guessed, we need to add a transport.menu.php file. Add one at /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;

Looks very similar to our transport.snippets.php file, except we're just returning one menu, and we're calling addOne on the menu object to add the Action as a related object to the menu. Note that the fields in each of the fromArray calls are the fields in the DB table for the menu and action, by the way.

So now our Menu and Action are all nicely packaged in.

Adding a Resolver

When someone installs our system, however, they're going to have 1 big problem - the database table modx_doodles isn't going to exist! Let's write a PHP resolver to create it on install. A PHP Resolver is a PHP script that runs after the Vehicle it's attached to has been installed. We'll attach this resolver to our Menu vehicle. Right below our $builder->createVehicle call for the Menu, and before you run putVehicle for that vehicle, add this:

$modx->log(modX::LOG_LEVEL_INFO,'Adding in PHP resolvers...');
$vehicle->resolve('php',array(
    'source' => $sources['resolvers'] . 'resolve.tables.php',
));

All that's passed into this PHP resolver is the 'source' field, which points to the PHP script. Let's create a file at /www/doodles/_build/resolvers/resolve.tables.php, and put this inside:

<?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;

Great. So here we're doing a few things. Note the initial check for $object->xpdo. $object is our Menu, since we attached this to the Menu's vehicle. Then we want to check for the xpdo var on it (which is also MODX). We then run into a switch statement, that checks a mysterious PACKAGE_ACTION const in the $options array. This little switch tells us to only run this code during new installs, or ACTION_INSTALL.

Further in the switch, we are assigning $modx as a reference to $object->xpdo, for easier typing. Then we'll find our Doodles' model path via our friendly getOption calls, and then run the addPackage call to add our xpdo schema to the database (remember that from Part I?). Finally, we'll run $modx->getManager(), which gets an xPDOManager instance, and call $manager->createObjectContainer('Doodle') on it.

This method tells MODX to run the SQL to create the database table for the Doodle class, which is what we want. And at the end of the resolver, we'll return true so that MODX knows everything went smoothly.

If you build the package, and install it now, it will create our database table. Great!

Adding the Changelog, Readme, License and Setup Options

Let's get fancy. When installing packages in MODX, often you'll see a dialog with a license, readme, and changelog. We want that in our package! First off, let's add those files.

Create a file in /www/doodles/core/components/doodles/docs/changelog.txt:

Changelog file for Doodles component.

Doodles 1.0
====================================
- Updating text, ready to build
- Added default properties to Doodles snippet in build
- Fixes to doodles class
- Fixed bugs with build, updated readme
- Initial commit

Then create a license file (we'll let you put the content in) at /www/doodles/core/components/doodles/docs/license.txt.

Finally, create a readme.txt in the docs/ directory:

--------------------
Extra: Doodles
--------------------
Version: 1.0

A simple demo extra for creating robust 3rd-Party Components in MODx Revolution.

Now that we've got our docs files, let's go to the end of our build.transport.php script, right before the $builder->pack() part, and add these lines:

$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',
    ),
));

So as you can see here, we have a setPackageAttributes() method, that allows some attributes. They're pretty self-explanatory - license takes in text for the license (which we grab using file_get_contents), readme takes in text for the readme, and changelog takes in text for the changelog.

The new one is the 'setup-options' array. First off, it's an array with a key of 'source' (like a resolver!), that points to a path of a PHP file (also like a resolver!). Let's create this PHP file, at /www/doodles/_build/setup.options.php:

<?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;

So, this looks familiar to a resolver, eh? That's because this little bit of code allows us to present 'Setup Options' to the user on installation. Right now we're just going to output a pretty message to tell people thanks for installing Doodles.

Remember that $options array in our PHP resolver? If we were to put any form elements in this output, they'd be found in that array with the same key. (An input with name of 'test' would be in $options['test']). That means you could make a resolver that would process the form fields you put in the Setup Options script.

That means that you could have a lot of neat little fields that do pre-installation options. That's a bit beyond the scope of this tutorial, but now that you know the basics, you can probably figure it out from there. (Also, plenty of existing Extras, such as Quip do this, and you can view their code to see how.

Summary

Now you can run your build.transport.php file, and you'll get a nice little doodles-1.0-beta4.zip file in your MODX install's core/packages/ directory. You can now either install that by uploading it to a MODX install's core/packages/ directory (but not the same one you just developed in!), or post it to modx.com/extras/ to be included in the official MODX Provider that hooks into Package Management. Pretty neat?

Let's recap. Over the 3 parts of this tutorial, we:

  • Stubbed out the directory structure for our Extra so we could develop externally and even get it on a source control system, such as Git
  • Added a custom xPDO model for our custom database table for our Doodles
  • Created a dynamic, templatable Snippet that lists our Doodles
  • Created a robust, CRUD-based Custom Manager Page to manage our Doodles from
  • Wrote a Transport Package (TP) build script to build our package with that installs our files and MODX objects
  • Made our TP create the custom DB table on install
  • Made our TP display the license/readme/changelog and a nice status message

All in all, I'd say that was pretty successful. Congrats, and we hope you enjoy developing on MODX!

The Doodles Extra in this tutorial can be found on GitHub, here: https://github.com/splittingred/Doodles/tree/2.1

This tutorial is part of a Series:

Suggest an edit to this page.