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
      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. 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 - MODX 2.1 and Earlier

Welcome to the MODX Documentation. It is an ongoing effort of the MODX community. If you would like to participate or if you notice any errors or missing content, please let us know.

This tutorial is part of a Series:

Overview

This tutorial is written as a comprehensive example on developing Extras for MODX Revolution - as well as how to setup your Extra to be easily packaged into a Transport Package, as well as able to be developed outside the MODX webroot so that source control (such as Git) can be used.

The overview will be dissecting the "Doodles" Extra, which is a simple Extra that uses a custom table to store objects called "Doodles", which have a name and a description. We'll have a Snippet that pulls them and displays a list of them that is templatable via a Chunk, a Custom Manager Page using ExtJS to have a CRUD grid for editing, and a build script for packaging. And we'll make it all i18n-compatible to allow easy translating. This is an extremely comprehensive tutorial, so if you're wanting only specific parts, use the Table of Contents above.

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

Setting Up Our Directories

There are many ways that you could start developing your Extra - you could do so straight out of MODX and package it with a packaging tool like PackMan, or you could alternatively develop it outside of the MODX webroot and put it in a source control system such as Git. This tutorial will be doing the latter method, as it is beneficial for a few reasons:

  • Allows immediate development straight from your Git repository
  • Allows easy collaboration between developers, as there is no copying files or changing core files - just developing in your preferred IDE and then doing some initial setup on paths.
  • Allows isolation of your code to be independent of MODX's core - so if you need to move it, you can do it in only one place.

Let's start. I've created a directory in my /www/ directory at: /www/doodles/

You can do it wherever you want, but for this tutorial I'll refer to /www/doodles/. Make sure that this directory is web-accessible locally, as you'll need that later on. I have /www/ setup as / on my localhost environment, for example.

You may have to add a System Setting in your MODX install called session_cookie_path and give it a value of "/" (no quotations). This will tell MODX to use the same session when you're running stuff at http://localhost/doodles/. Also, giving it a unique name via session_cookie_name (like "modxlocaldevsession") is a good idea too. That'll prevent conflicts with other MODX installs on your local machine. If you do this, empty the core/cache/ directory and relogin after doing so.

In here, we'll have quite a few directories:

Let's note a few things. First off, our main 3 directories are core/, assets/ and _build/. Let's explain a few things about how MODX Extras work - typically (although this does not have to be done), Extras are separated into 2 different directories when installed: core/components/myextra/ and assets/components/myextra/. Why? Well, the assets/components/ directory only contains the web-specific assets - JavaScript files, CSS files, etc. These are the files you want publically accessible to the web. All the PHP files, classes, maps, and other files, however, are in the core/components/ directory. This keeps those files secure in the core/ directory (which can be moved outside the webroot in some MODX installations for even more security).

They're separated into our directory structure here to mimic how they will be in the MODX installation after it's installed by the Transport Package.

The _build/ directory isn't even packaged into the zip file after we create a Transport Package for it. It's there primarily for building the Transport Package. We'll get into that near the end of the tutorial.

Let's dive deeper into each subdirectory. In the assets/ directory, the only not-obvious file is the connector.php. This file will allow us to have custom processors for our Custom Manager Page (CMP) we'll be writing. We'll get more into that later.

In the core/components/doodles/ directory, we have a few directories worth explaining:

  • controllers - These are the controllers for our CMP. More on those later.
  • docs - Just contains a changelog, readme and license file.
  • elements - All our Snippets, Chunks, Plugins, etc.
  • lexicon - All our i18n language files. More on those later.
  • model - Where all of our classes lie, as well as our XML schema file for our custom database tables.
  • processors - All our custom processors for our CMP.

Let's also note that this directory is completely outside of our MODX webroot (mine's installed in /www/modx/). So - yes. You can run "git init" and make yourself a Git repository out of the /www/doodles/ directory (or whatever you made it). And you can push that up, without having to worry about it (though there's a few files we'll talk about later you'll want to add to a .gitignore file).

There we have it. A completely isolated development environment from MODX so that we can do separate development and seamless collaboration. Let's get further in.

Creating the Doodles Snippet

Okay. Go ahead and create a Snippet file in:

/www/doodles/core/components/doodles/elements/snippets/snippet.doodles.php

You'll have to make a snippets/ directory, if you haven't already. Your file should be empty, but let's add a few lines of code:

$doodles = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties);
if (!($doodles instanceof Doodles)) return '';

Whoa! What's that? Well, it's where the magic happens. Let's break down each part. First off, we have the getService call. It's shorthand notation right now, so let's split it up a bit to make it easier to read:

$defaultDoodlesCorePath = $modx->getOption('core_path').'components/doodles/';
$doodlesCorePath = $modx->getOption('doodles.core_path',null,$defaultDoodlesCorePath);
$doodles = $modx->getService('doodles','Doodles',$doodlesCorePath.'model/doodles/',$scriptProperties);

Okay, so first off, what is $modx->getOption? That's a method that grabs the System Setting with the key (the first parameter). In the first line, we are grabbing a 'default' path we are assuming our Doodles core path is going to be, by prefixing the MODX core path to it. It'll be: /www/modx/core/components/doodles/

Next, we'll pass that as a fallback value for the next getOption call. This one passes 3 parameters: a key named "doodles.core_path", null, and our default path var we just assigned. In getOption, the 2nd parameter is an array to search for the key (which we aren't doing, so we can set it to null), and the 3rd paramter is a default value if the key isn't found.

So, for right now, our 2nd line will return /www/modx/core/components/doodles/. But that's not where our Doodles core path is! (hint: it's at /www/doodles/core/components/doodles). We want to tell these lines to find it there. So what do we do?

Making the Magic Path Settings

We set a couple System Settings (that are specific to our development environment) that tell these lines where to get our files! Go ahead and make the following System Settings and set their values:

  • doodles.core_path - /www/doodles/core/components/doodles/
  • doodles.assets_url - /doodles/assets/components/doodles/

If you need to change either of those to reflect your correct paths, such as the URL one, do so. Now our first line will return: /www/doodles/core/components/doodles/ Bingo! Cool, huh?

Why do we do this? Why not just refer to /www/doodles/core/components/doodles/? Well, that wouldn't work in someone else's installation. There's is most likely to be at MODXPATH/core/components/doodles/. Our Transport Package (later) will handle all of that dynamic path stuff, but we want to add an override to allow us to develop Doodles outside of the MODX path. And we just did. Coding bliss!

Now on to the third line:

$doodles = $modx->getService('doodles','Doodles',$doodlesCorePath.'model/doodles/',$scriptProperties);

Okay, this gets crazy. $modx->getService loads a class and instantiates an object of it, if it exists, and sets it to $modx->doodles here in this case (the first parameter passed in). More on getService can be found here. But wait! We don't have a Doodles class! Well, it's time to make one.

Making the Doodles Base Class

First off, you're probably asking me why we're even making this class. Well, it'll help for a few reasons: we can define some basic paths in it that we'll use across our custom Extra, and it can also give us some app-wide methods we can use. Trust me, it's useful. So let's make it in /www/doodles/core/components/doodles/model/doodles/doodles.class.php:

class Doodles {
    public $modx;
    public $config = array();
    function __construct(modX &$modx,array $config = array()) {
        $this->modx =& $modx;

        $basePath = $this->modx->getOption('doodles.core_path',$config,$this->modx->getOption('core_path').'components/doodles/');
        $assetsUrl = $this->modx->getOption('doodles.assets_url',$config,$this->modx->getOption('assets_url').'components/doodles/');
        $this->config = array_merge(array(
            'basePath' => $basePath,
            'corePath' => $basePath,
            'modelPath' => $basePath.'model/',
            'processorsPath' => $basePath.'processors/',
            'chunksPath' => $basePath.'elements/chunks/',
            'jsUrl' => $assetsUrl.'js/',
            'cssUrl' => $assetsUrl.'css/',
            'assetsUrl' => $assetsUrl,
            'connectorUrl' => $assetsUrl.'connector.php',
        ),$config);
        $this->modx->addPackage('doodles',$this->config['modelPath']);
    }
}

Great! It's pretty simple for now - just creates a class object that has a constructor that sets a reference to the modX object at $doodles->modx. This is useful later. Also, it populates some basic paths we may use later on into the $doodles->config array, and it does it with our fancy System Settings trick so we can point it to our /www/doodles/ path!

Now, back to our Snippet. Let's go ahead and add some default properties to our Snippet, after the lines above, so it looks like this:

$dood = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties);
if (!($dood instanceof Doodles)) return '';

/* setup default properties */
$tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl');
$sort = $modx->getOption('sort',$scriptProperties,'name');
$dir = $modx->getOption('dir',$scriptProperties,'ASC');

$output = '';

return $output;

Cool. Now we want to use xPDO to query the database to grab our records...oops. We haven't made an xPDO model for them yet. We should do that.

Making the Model

xPDO does database abstraction into neat OOP query methods. It currently is beginning to support multiple databases, and it does that by the abstraction of DB queries. Also, it allows you to keep your DB rows in nice, clean classes and do all kinds of neat things in very short lines of code. But to do that, we have to add an xPDO model to our Snippet (via the $modx->addPackage method). But first we have to build that model, using an xPDO Schema. There's a nice long tutorial here on how to do that, but we'll go over it fast for now.

Go ahead and make a xml file in /www/doodles/core/components/doodles/model/schema/doodles.mysql.schema.xml. Put this in it:

<?xml version="1.0" encoding="UTF-8"?>
<model package="doodles" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM">
    <object class="Doodle" table="doodles" extends="xPDOSimpleObject">
        <field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/>
        <field key="description" dbtype="text" phptype="string" null="false" default=""/>

        <field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
        <field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />

        <aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
        <aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
    </object>
</model>

Ooookay. Lots of stuff here. First off, the first line:

<model package="doodles" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM">

This tells the schema that our xPDO package is called 'doodles'. This is what we'll refer to in our addPackage() call. Great. It also says the base class for all the objects defined here is "xPDOObject", and that this schema is made for MySQL. Finally, it gives a default MySQL engine of MyISAM. Next!

<object class="Doodle" table="doodles" extends="xPDOSimpleObject">

An "object" in a xPDO schema is basically a database table. This line says, give xPDO a name for the table called '{table_prefix}doodles'. Assuming your table prefix you did in your MODX install is 'modx', it would translate to 'modx_doodles'. Then it says that it extends "xPDOSimpleObject". What's that? Well, xPDOObject is the base object for any xPDO table class. xPDOSimpleObject extends it, but adds a nice little "id" auto-increment field to that table. So, since we're gonna want a "id" field on our table, we use xPDOSimpleObject.

<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/>
<field key="description" dbtype="text" phptype="string" null="false" default=""/>
<field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
<field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />

The rest of these fields are pretty self-explanatory - they are fields on the DB table. Let's move on to the last two parts:

<aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
<aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>

Okay, this is where related objects come in with xPDO. For the purposes of this tutorial, just know that this tells xPDO that the createdby field maps to a modUser, and the editedby field maps to another modUser. Cool? Now let's get into parsing that xml file and creating our classes and maps.

The Schema Parsing Script

Now it's time to look at our elusive _build directory. Go ahead and create a file in there: /www/doodles/_build/build.schema.php and put this in:

<?php
require_once dirname(__FILE__).'/build.config.php';
include_once MODX_CORE_PATH . 'model/modx/modx.class.php';
$modx= new modX();
$modx->initialize('mgr');
$modx->loadClass('transport.modPackageBuilder','',false, true);
echo '<pre>'; /* used for nice formatting of log messages */
$modx->setLogLevel(modX::LOG_LEVEL_INFO);
$modx->setLogTarget('ECHO');

$root = dirname(dirname(__FILE__)).'/';
$sources = array(
    'model' => $root.'core/components/doodles/model/',
    'schema_file' => $root.'core/components/doodles/model/schema/doodles.mysql.schema.xml',
);
$manager= $modx->getManager();
$generator= $manager->getGenerator();

if (!is_dir($sources['model'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Model directory not found!'); die(); }
if (!file_exists($sources['schema_file'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Schema file not found!'); die(); }
$generator->parseSchema($sources['schema_file'],$sources['model']);

echo 'Done.';
exit();

Basically this file parses your XML schema file and makes xPDO classes and maps (PHP representations of that XML file) for your component. We'll come back to this, but first off, it's not gonna run. It's gonna die on looking for a /www/doodles/_build/build.config.php file. Time to make one of those!

<?php
define('MODX_BASE_PATH', '/www/modx/');
define('MODX_CORE_PATH', MODX_BASE_PATH . 'core/');
define('MODX_MANAGER_PATH', MODX_BASE_PATH . 'manager/');
define('MODX_CONNECTORS_PATH', MODX_BASE_PATH . 'connectors/');
define('MODX_ASSETS_PATH', MODX_BASE_PATH . 'assets/');

define('MODX_BASE_URL','/modx/');
define('MODX_CORE_URL', MODX_BASE_URL . 'core/');
define('MODX_MANAGER_URL', MODX_BASE_URL . 'manager/');
define('MODX_CONNECTORS_URL', MODX_BASE_URL . 'connectors/');
define('MODX_ASSETS_URL', MODX_BASE_URL . 'assets/');

Obviously, you may need to change those paths to wherever your MODX installation is at.

Now, you can go to your _build/build.schema.php file, and run it. I do it by loading up in a web browser: http://localhost/doodles/_build/build.schema.php. You may need to change that URL to wherever you made the doodles directory web-accessible (you did like I said to earlier, didn't you? If not, now's a good time!).

That should run and generate you some nice pretty class files and maps:

Bravo! You've just made your maps and classes. Let's go make an adjustment to our Doodles base class, so it automatically adds in the Doodles xPDO package whenever we load the class. Add this line after the $this->config = array_merge part, at the end of the constructor:

$this->modx->addPackage('doodles',$this->config['modelPath']);

This tells xPDO that we want to add the 'doodles' xPDO package in, allowing us to query that custom table. Bravo!

Okay, our Snippet so far looks like this:

$dood = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties);
if (!($dood instanceof Doodles)) return '';

/* setup default properties */
$tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl');
$sort = $modx->getOption('sort',$scriptProperties,'name');
$dir = $modx->getOption('dir',$scriptProperties,'ASC');

$output = '';

return $output;

Pretty lame snippet, eh? Well, all we're doing right now is setting up the Doodles class object into a variable called $dood, and setting up some defaults for properties we'll use later. $scriptProperties is an array, by the way, of all the properties passed into the Snippet. The getOption calls here parse it to find the properties in them, and if not set, gives them default values.

The include Snippet

You're probably also thinking, "It's not even in the MODX manager yet! Who is this guy and who does he think he's kidding?" Good question. Well, let's do that then!

Now, to preserve our custom System Setting-based paths we did earlier, we're not going to want to actually create a Doodles snippet in our manager, and paste its code in there. That'd make it pretty annoying to develop - lots of copy+paste, etc. So, we're going to make a generic "include" snippet that looks like this:

$o = include $file;
return $o;

That's it. Seriously. Now, create a Resource called "Doodles" with an alias of "doodles" in your MODX install and put this in it:

[[!include? &file=`[[++doodles.core_path]]elements/snippets/snippet.doodles.php`]]

This says, "Include and run the file in the Doodles core path System Setting we set at earlier, plus concatenate to the end of that path 'elements/snippets/snippet.doodles.php'". That'll resolve to:

/www/doodles/core/components/doodles/elements/snippets/snippet.doodles.php

Which works perfect for us! Then we can edit our Snippet in our favorite IDE and go about our business. It'll also pass any properties we pass into the include snippet call into our Doodles snippet as well. Cool! Back to the Snippet.

Building the Query

First off, we need to create the table. This will be done in a resolver later, but for now, just add this to your snippet, before the return statement:

$m = $modx->getManager();
$created = $m->createObjectContainer('Doodle');
return $created ? 'Table created.' : 'Table not created.';

Then go ahead and run your snippet. This will automatically create the DB table that we made in our schema. Now that you've got that done, remove that code and we can continue on.

Okay, let's add this to our Snippet before the return statement:

$doodles = $modx->getCollection('Doodle');
$output = count($doodles);

That's going to grab an array of Doodle objects, or in non-xPDO terms, a bunch of rows from the database. Go ahead and save your snippet, then run it in the browser at http://localhost/modx/doodles.html (or wherever the Resource was). You should get this:

0

Ha, tricked you! In reality, the first time it runs it wont grab anything, since we don't have any data in the table. Let's go put some data in the table.

Use whatever DB editing software (such as phpMyAdmin) you want, and find the 'modx_doodles' table in your database. Add a few rows to it (just add name/description values for now). That should give you some data. Let's assume you added 2 rows. Go ahead and run your snippet, and you should get:

2

Great! Your custom database query works! Let's make it more complex. We can use xPDO's xPDOQuery to create some pretty complex queries. For now, let's just add a sort command to it:

$c = $modx->newQuery('Doodle');
$c->sortby($sort,$dir);
$doodles = $modx->getCollection('Doodle',$c);

Great. That will sort it by the field in $sort (which we defined above) and the direction in $dir. Now we need to actually create some output for it. Let's do it!

The Doodles class getChunk Method

In a lot of my Extras, I add a couple of helper methods to my base class called getChunk. What they allow me to do is use file-based chunks to develop in. So, let's do that. Go ahead and open up your Doodles class and add these two methods in:

    public function getChunk($name,$properties = array()) {
        $chunk = null;
        if (!isset($this->chunks[$name])) {
            $chunk = $this->_getTplChunk($name);
            if (empty($chunk)) {
                $chunk = $this->modx->getObject('modChunk',array('name' => $name));
                if ($chunk == false) return false;
            }
            $this->chunks[$name] = $chunk->getContent();
        } else {
            $o = $this->chunks[$name];
            $chunk = $this->modx->newObject('modChunk');
            $chunk->setContent($o);
        }
        $chunk->setCacheable(false);
        return $chunk->process($properties);
    }

    private function _getTplChunk($name,$postfix = '.chunk.tpl') {
        $chunk = false;
        $f = $this->config['chunksPath'].strtolower($name).$postfix;
        if (file_exists($f)) {
            $o = file_get_contents($f);
            $chunk = $this->modx->newObject('modChunk');
            $chunk->set('name',$name);
            $chunk->setContent($o);
        }
        return $chunk;
    }

For now, all you need to know is that these methods will look for Chunks in your /www/doodles/core/components/doodles/elements/chunks/ directory, postfixed with '.chunk.tpl' and all in lowercase. If it doesn't find them on the filesystem, it looks for them in MODX. So, if we called:

$o = $dood->getChunk('hello',array('name' => 'Joe'));

It would set to $o the contents of /www/doodles/core/components/doodles/elements/chunks/hello.chunk.tpl, with the property [[+name]] parsed as Joe. This will allow you to edit your Chunks in your IDE, rather than in MODX. It will also allow you to package your Extra without installing default chunks into the user's MODX install (which they would be tempted to overwrite, which would get erased in upgrades of your Extra).

So, back to our snippet. Create a Chunk file in /www/doodles/core/components/doodles/elements/chunks/rowtpl.chunk.tpl, and put this inside:

<li><strong>[[+name]]</strong> - [[+description]]</li>

Now add this below your query but above the return line in your Snippet:

foreach ($doodles as $doodle) {
    $doodleArray = $doodle->toArray();
    $output .= $dood->getChunk($tpl,$doodleArray);
}

So, what this does is iterates over all the Doodle objects we got with the getCollection call, and creates a PHP array from their values with the toArray method. Then, it uses getChunk and that array to set values to the Chunk for each row, and append that to the $output variable. So we get a bunch of \<li\> tags (as many as you added rows in the DB for). It should look something like this:

Cool, huh? You can obviously change that Chunk to whatever you want - and people can pass in a name of a Chunk to &tpl in their Snippet call to use whatever Chunk they want. Templatability in your Snippet! Hooray!

So let's recap. Our snippet looks like this:

<?php
$dood = $modx->getService('doodles','Doodles',$modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/').'model/doodles/',$scriptProperties);
if (!($dood instanceof Doodles)) return '';

/* setup default properties */
$tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl');
$sort = $modx->getOption('sort',$scriptProperties,'name');
$dir = $modx->getOption('dir',$scriptProperties,'ASC');

/* build query */
$c = $modx->newQuery('Doodle');
$c->sortby($sort,$dir);
$doodles = $modx->getCollection('Doodle',$c);

/* iterate */
$output = '';
foreach ($doodles as $doodle) {
    $doodleArray = $doodle->toArray();
    $output .= $dood->getChunk($tpl,$doodleArray);
}

return $output;

And we've got it loading our custom base class from our System Setting-defined paths, adding our custom xPDO db package, pulling from our custom database table, and outputting it via a Chunk. Cool, huh?

Summary

We've got ourselves a nice custom database model, which our Doodles Snippet using to grab Doodles records from our database. We also have looked at the basic structure for a comprehensive MODX Extra.

But we're gonna want some way of editing that data in the MODX manager, right? Well, that's where Custom Manager Pages (CMPs) come in. Go on and proceed to the next part of this tutorial.

This tutorial is part of a Series: