MODX CMPs — An Anatomy Lesson

I’ve spent a lot of time wading through arcane and labyrinthine code trying to understand the structure of MODX Custom Manager Pages (CMPs). If I get away from them for a while, I get lost all over again, so I thought I’d put this article up to help others and to serve as a reference when I forget how they work.

Part of the problem is that there are so many files involved and each one often has only a small part of the code.

In this article, I’ll walk through the structure and abbreviated code of the Example CMP included with MyComponent. I based it on the structure of a number of different MODX extras. It may help you to see the structure and some code all in one place rather than wandering back and forth through the files. Here’s an abbreviated version of the file structure of one typical CMP (> indicates a directory).

Mark Hamstra and Alan Pich have been kind enough to point out that this article is out of date with respect to MODX 2.2.x. Some of the arcane twists described here are no longer necessary. I plan to update the CMP that comes with MyComponent and rewrite this article when I have time. In the meantime, this may serve to help people understand the structure of some of the older MODX CMPs like Batcher.

Directory Structure

> assets/components/example
    > js
        > sections
            home.js
        > widgets
            chunk.grid.js
            home.panel.js
            snippet.grid.js
    connector.php

> core/components/example/
    > controllers
        > mgr
            header.php
            home.php
    > lexicon
        > en
            default.inc.php

    > model
        > example
            > request
                examplecontrollerrequest.class.php
            example.class.php
    > processors
        > mgr
            > chunk
                changecategory.class.php
                getlist.class.php
                remove.class.php
            > snippet
                changecategory.class.php
                getlist.class.php
                remove.class.php
If you install MyComponent you can look at the files of the Example CMP to help you understand this article. You’ll need to build and install the Example project as described here, but the process is fairly simple. See the “Building and Installing the Example Project” section.

The Files

The Action file

The first file involved in the process is the index.php file that will be executed when the user clicks on the Example CMP menu item under “Components” in the Manager’s Top Menu. Its name is set in Manager | Actions and its location is based on the path specified in the Example namespace: {core_path}components/example/. It usually contains just two lines of code.

core/components/example/index.php (main action file, basically just a proxy for the the controllers index.php file – this is the whole file):

$o = include dirname(__FILE__).'/controllers/index.php';
return $o;

I’m sure there’s a good reason for not putting the controller code in the index.php action file directly, but I don’t know what it is, just that I’ve always seen it done this way in MODX.

The Controller File

You can think of a typical MODX controller file as overseeing the display. It displays information and usually injects the JavaScript that processes user actions. The form it displays often has the equivalent of many "Submit" buttons and sometimes input fields. When one of the pseudo-submit buttons is triggered, the controller’s request handler hands off the processing (via JavaScript Ajax requests) to another file containing PHP code and changes the display based on what that PHP code returns.

In a simple CMP, the controller’s request handler may call a connector file, which in turn calls a processor. More often, the JavaScript on the page calls the connector directly via Ajax and the connector calls the processors. The processor interacts with the database and then returns information to the connector and the connector passes that information, unchanged, back to the controller or to the JavaScript on the page.

For example, the controller for the "Manage Users" form in the Manager displays user information in a grid. The search filter is an input field and there are many actions you can perform by clicking or right-clicking on various parts of the form. Depending on the action, the formfires off an Ajax request to the appropriate processor (via a connector) and alters the form based on what the processor returns. The processor queries and sometimes alters the MODX database before returning anything.

The controller for the Example CMP does essentially the same thing, but with chunks and snippets rather than users.

core/components/example/controllers/index.php is the “real” controller file we saw included above. It instantiates the Example class and returns what comes back from its initialize() method, which returns what comes back from the examplecontrollerrequest class’s handleRequest() method, which calls some processor via the Example controller file. In simpler terms, the controller calls the example class’s constructor and then its initialize() method and returns what comes back from initialize() to be displayed in the Manager panel:

require_once dirname(dirname(__FILE__)).'/model/example/example.class.php';
$example = new Example($modx);
return $example->initialize('mgr');

The Class file

core/components/example/model/example.class.php (the Example class file’s constructor and initialize() methods called in the code above):

function __construct(&$modx, $config = array()) {
    /* All $this->config settings set here */
    /* addPackage() called */
}

/* Loads the controller request class and returns what comes back
its handleRequest() method. If no user action has occurred yet,
handleRequest() will perform the default action. */

function initialize($context = 'mgr') {
    if (!$this->modx->loadClass('example.request.ExampleControllerRequest',
                $this->config['modelPath'],true,true)) {
        return 'Could not load controller request handler.';
    }
    $this->request = new ExampleControllerRequest($this);
    $output = $this->request->handleRequest();

    return $output;
}

The Request Handler

core/components/example/model/example/request/examplecontrollerrequest.class.php (the request handler – handles all requests fired by the JS in the displayed panel):

$defaultAction = 'home';

function handleRequest() {
    $this->loadErrorHandler();
    $this->action = isset($_REQUEST[$this->actionVar])
        ? $_REQUEST[$this->actionVar]
        : $this->defaultAction;
    return $this->_respond();
}

/* Gets the Header for the display from header.php and the results of the action and
returns them as a single string */
function _respond() {
    $viewHeader = include $this->example->config['corePath'] .
        'controllers/mgr/header.php';

    $f = $this->example->config['corePath'] . 'controllers/mgr/' .
        $this->action.'.php';
    if (file_exists($f)) {
        $viewOutput = include $f;
    } else {
        $viewOutput = 'Action not found: '.$f;
    }

    return $viewHeader.$viewOutput;
}

The header.php file

core/components/example/controllers/mgr/header.php (kind of like a template file for CMPs — stuff that will be in the displayed panel, no matter what):

/* load the css file */
$modx->regClientCSS($example->config['cssUrl'].'mgr.css');
/* load the general JS file */
$modx->regClientStartupScript($example->config['jsUrl'].'example.js');

/* Make the config settings available to the JS code --
   this is how you pass any necessary PHP variables
   to the JavScript */
$modx->regClientStartupHTMLBlock('
<script type="text/javascript">
    Ext.onReady(function () {
        Example.config = ' . $modx->toJSON($example->config).';
        Example.config.connector_url = "' . $example->config['connectorUrl'].'";
    });
</script>');
/* Since this file gets no data, it doesn't need to return anything */
return '';

The home.php File

core/components/example/controllers/mgr/home.php (The “Home Page” of the operation – loads the JS widgets and returns the HTML code that the JS will replace to make the display):

$modx->regClientStartupScript($example->config['jsUrl'].'widgets/home.panel.js');
$modx->regClientStartupScript($example->config['jsUrl'].'sections/home.js');
$modx->regClientStartupScript($example->config['jsUrl'] . 'widgets/chunk.grid.js');
$modx->regClientStartupScript($example->config['jsUrl'] . 'widgets/snippet.grid.js');

$output = '
<div id="example-panel-home-div"></div>';

return $output;

Notice the $output variable returned at the end of the code above. It contains nothing but the minimal text for the example-panel-home-div div. If you do a “View Source” on a manage page, this is often all that you’ll see of the CMP. The full panel is created on the fly by the JS code.

The example.js file

assets/components/example/js/example.js (The general JS file loaded above – does some light modExt housekeeping:

var Example = function (config) {
    config = config || {};
    Example.superclass.constructor.call(this, config);
};
Ext.extend(Example, Ext.Component, {
    page: {}, window: {}, grid: {}, tree: {}, panel: {}, combo: {}, config: {}
});
Ext.reg('example', Example);

var Example = new Example();

The upshot of all the code above, the first time through, is that all the CSS and JS is loaded, the config array (think $scriptProperties) is set up, necessary classes are loaded and initialized and all that’s actually returned to be displayed in the Manager’s right-hand panel is:

'
<div id="example-panel-home-div"></div>'

The JS code that’s been loaded will insert the appropriate JS widgets into that div. The widgets will put grids, buttons, links, input fields, and context menus on the page. When they are selected by the user, they either modify what’s there, or fire an Ajax request. Almost none of the above actually *does* anything except create the display the user sees and set up the JS that listens for user actions. The one action that’s performed is the default action specified in the request handler, which in the case of the Example CMP is to call the getList processor to fill the initial data in the grids.

home.js and the Widgets

To conserve space, I’ve left out the home.js file and the three widget files. They can be found in the assets/components/example/js/ directory.

The home.js file is the basically just a container for the main view. That view is contained in the home.panel.js file, which in turn contains the tabs that hold the two other widgets: snippet.grid.js and chunk.grid.js. Those two widgets contain the JavaScript code for the actual grids. They render the grids and specify the actions that will be performed when the user selects something in one of the grids.

The Connector

The real action is performed by the processors (discussed below), which are called with Ajax specified in the widget’s JS code (typically via a connector file). The connector (along with any JS files) lives in the assets directory because it has to be available by URL for the Ajax. The connector file just serves as a gateway to the processors.

assets/components/example/connector.php (the connector file):

/* Because it's a new request, we have to
instantiate MODX and the Example class
before calling the appropriate processor. */

/* include core.config.php to get the core path
   and config key constants */
require_once dirname(dirname(dirname(dirname(__FILE__)))) .
    '/config.core.php';

/* Load the MODX config file */
require_once MODX_CORE_PATH .
    'config/' . MODX_CONFIG_KEY . '.inc.php';

/* load the main MODX indes.php file, which instantiates MODX,
   and gets and sanitizes the actual $request */
require_once MODX_CONNECTORS_PATH .
    'index.php'; /*  */

/* load and instantiate our Example class */
$exampleCorePath = $modx->getOption('example.core_path',
    null, $modx->getOption('core_path') .
    'components/example/');
require_once $exampleCorePath .
    'model/example/example.class.php';
$modx->example = new Example($modx);

/* load the Example default lexicon file */
$modx->lexicon->load('example:default');

/* handle request, after getting the processors path */
$path = $modx->getOption('processorsPath',
    $modx->example->config, $exampleCorePath .
    'processors/');
$modx->request->handleRequest(array(
    'processors_path' => $path,
    'location' => '',
));

The Processors

Processors do the real work for the CMP. They interact with the MODX database — creating, updating, or removing MODX objects (rows) in the database tables.

The final part of the code just above “handles” the request by calling a processor. The processors are located under:

core/components/example/processors/

So, in the chunk grid widget’s JS code, this line:

action: 'mgr/chunk/getlist'

results in the processor at core/components/example/processors/mgr/chunk/getlist.class.php being “included” and its initialize(), then process() methods being called. That class extends modObjectGetListProcessor, which extends modObjectProcessor. All the Example getlist class does is override the prepareRow() method, so it’s really the ancestors’ initialize() and process() methods that will execute.

The getList processor queries the database to get a list of the appropriate objects, converts the list to a JSON string, and returns it. Create and update processors do just what their names suggest. The names are arbitrary and a processor can do literally anything you can do in PHP.

Unlike most other MODX objects, Resources have both a remove and a delete processor. The delete processor just marks the resource as deleted and does nothing else. The resource appears in the tree with a line through it. When you empty the trash in the Resource tree, the remove processor is called for each deleted resource. It deletes the resources from the database and also deletes their related objects (for example, deleting any TV values for the resource).

Whatever is returned from the process() method is returned as a JSON string for the original AJAX call in the JS.

For a simple class-based processor, you can just extend modProcessor, implement (override) just the process() method, and have it return something (sometimes just a success or failure indicator).

Processor Flavors

There are two “flavors” of processors in MODX, class-based and procedural. When executing a processor request, MODX will look for a class file (e.g., getlist.class.php). If it finds it, it will call its initialize() and process() methods automatically and return what comes back from process(). If it doesn’t find a class, it will look for a regular .php file with the same name and simply “include” it (in the example above it would be getlist.php). The PHP file will have a return statement at the end to return a JSON string to the JS Ajax request.

The new class-based processors used in the examples above can take advantage of the many useful methods of the classes they extend. Often, you only need to override one or two (sometimes zero) of the methods in your own processor, which can save a lot of time and trouble. See Mark Hamstra’s excellent article on them here.

If you look at the part of the MODX class file that runs processors, you’ll see that the procedural (non-class-based) processors are based on a class called modDeprecatedProcessor, which suggests that they will be removed in the future and all processors will be in the form of classes.

Do I Really Need All That?

Almost none of the stuff above is strictly necessary. MODX will display whatever is returned from the action index.php file in the CMP panel in the Manager’s right-hand panel and it can be plain old HTML. The SiteCheck CMP has an index.php file that works like a regular PHP form-processing snippet. The index.php file instantiates the $modx object and its own class, loads the CSS and Lexicon, displays a form, responds to the $_POST when the form is submitted, and prints output below the form – no ExtJS, no processors, no controllers, and no connectors.

SiteCheck has code at the top that throws you out if you’re not logged in to the Manager. It works fine and I think it’s as secure as any other CMP. That said, using the structure describe in this article with modExt provides many powerful UI options that would be *very* difficult to duplicate on your own. SiteCheck gets by without it because it simply produces a report and doesn’t interact with the user at all except through the form and some links in the report.

CMP Security

There *are* security implications for many CMPs, however, especially if the user hasn’t relocated the core outside of the web root. In that case, a remote user may be able to execute your action files after setting $_POST variables and cause serious trouble or penetrate your site. You need to think carefully about what can be done remotely with your files and always sanitize any $_REQUEST, $_GET, or $_POST variables you use.


For more information on how to use MODX to create a web site, see my web site Bob’s Guides, or better yet, buy my book: MODX: The Official Guide.

Looking for quality MODX Web Hosting? Look no further than Arvixe Web Hosting!

Tags: , , , , , , , | Posted under MODX, MODX | RSS 2.0

Author Spotlight

Bob Ray

Bob Ray

I am the author of MODX: The Official Guide and over 30 MODX add-on components. I host Bob's Guides, a source of valuable information for MODX users, and I've been very active in the MODX Forums with over 18,000 posts.

2 Comments on MODX CMPs — An Anatomy Lesson

  1. Alan Pich says:

    Bob I’m surprised… a very well written article for sure, but as the author of the Official guide, shouldnt you be advocating the more recent (2.2.6+ if i remember correctly) class based approach. MODX has enough standards compliance issues as it is without tutorials based on outdated methods

  2. Bob Ray Bob Ray says:

    All of the processors in the article and the main controller request handler *are* class based. I wrote the article a while ago when many CMPs were not even using those.

    I’m not aware of any announcement that the use of class-based processors, connectors, and controllers is a MODX standard that CMP developers are expected to comply with (though things are certainly heading in that direction).

Leave a Reply

Your email address will not be published. Required fields are marked *


7 − 7 =

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>