MODX 2.2 CMPs: An Anatomy Lesson

A while back, I wrote an article describing the anatomy of MODX Revolution CMPs. The article was based on the structure of pre-MODX 2.2 CMPs and it still has some value for people trying to understand how older components like Batcher work.

In this article, we’ll look at the architecture of post-MODX 2.2 CMPs, which are much simpler, and also more secure than older ones. My Thanks to Mark Hamstra who was a great help in explaining the finer points of the conversion to class-based action and controller files.

MODX logo

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 for me when I forget how they work. The newer-style CMPs are simpler and easier to understand, but it’s still nice to have some notes on how they work.

In the sections below, I’ll walk through the structure and abbreviated code of the updated Example CMP included with MyComponent. It may help you to see the structure and some code all in one place rather than wandering back and forth through the files.

Overview

Before we look at the file structure, here’s a quick rundown of how a typical Ajax-based CMP works. When you click on a menu item in MODX 2.2+, MODX checks the name of the action file and the namespace path of the menu item. The action file is almost always called index, so MODX looks in the namespace path for a file called index.class.php.

That index.class.php file is essentially a router that calls the appropriate controller (often, there’s only one controller). The controller does some housekeeping (e.g., checking permissions and loading lexicon files), displays whatever goes on the initial CMP panel, and loads the appropriate JavaScript. The JavaScript responds to user actions by calling the appropriate processor (usually by sending the name/location of the processor in an Ajax request to a connector, which in turn runs the processor). The processor returns results to the connector and the connector returns them to the JavaScript Ajax call that made the request. The JS code then alters the display depending on what it received.

Here’s an abbreviated version of the file structure of the Example project’s CMP (> indicates a directory).

Directory Structure

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

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

    > model
        > example
            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
    > templates
        mgr.tpl
    index.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. If you have already installed MyComponent and created the Example project, you may need to delete the Example project and re-create it to avoid having some of the old-style files cluttering up the directories. They won’t hurt anything, but they’ll make this explanation harder to follow.

The Files

Here’s an explanation of the basic files used by a typical modExt-based CMP using the CMP included with MyComponent’s Example project.

The Action file

The first file involved in the process is the index.class.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/.

The index.class.php file replaces the old index.php file that simply ‘included’ the controller index file. Having this as a class file rather than an executable PHP file is more secure because executing it does nothing at all. The index.class.php file serves as a router for actions coming from the Manager. Menu items in the Manager’s top menu send their actions to the action file in the URL, so several different menu items could use the same action file. Here’s the abbreviated code of the Example CMP’s index.class.php file:

abstract class ExampleManagerController extends modExtraManagerController {
    /** @var Example $example */
    public $example = NULL;

    /* Initialize the main manager controller.*/

    public function initialize() {
        /* Instantiate the Example class in the controller */
        $path = $this->modx->getOption('example.core_path',
                NULL, $this->modx->getOption('core_path') .
                'components/example/') . 'model/example/';
        require_once $path . 'example.class.php';
        $this->example = new Example($this->modx);

        /* Optional alternative  - install PHP class as a service */

        /* $this->example = $this->modx->getService('example',
             'Example', $path);*/

        /* Add the main javascript class and our configuration */
        $this->addJavascript($this->example->config['jsUrl'] .
            'example.class.js');
        $this->addHtml('<script type="text/javascript">
        Ext.onReady(function() {
            Example.config = ' . $this->modx->toJSON($this->example->config) . ';
        });
        </script>');
    }

    public function getLanguageTopics() {
        return array('example:default');
    }

    public function checkPermissions() {
        return true;
    }

    public function getTemplateFile() {
        return $this->example->config['templatesPath'] . 'mgr.tpl';
    }
}

/**
 * The Index Manager Controller is the default one that gets
 * called when no action is present. */

class IndexManagerController extends ExampleManagerController {

    /* Defines the name or path to the default controller to load. */
    public static function getDefaultController() {
        return 'home';
    }
}

Notice that there are two classes declared in the code above. The abstract class ExampleManagerController and near the end, the IndexManagerController class which extends the abstract class.

The abstract class contains the default behaviors for all your controllers. The concrete class must be called IndexManagerController. MODX calls it when there is no action specified in the request. It’s only function is to tell MODX the name of the default controller to call.

Let’s take a quick look at what the code of the abstract class above actually does. First, it instantiates the Example PHP class for use in any controller that’s called (or installs it as a service). You could also call the Example class initialize() method here if necessary. Usually, the $scriptProperties array is passed to the constructor, which places it (along with any other necessary values) in a variable called $this->config so it can be made available to the JS code using a method we’ll see in just a bit. Often, paths and values used in the JS code are set here as in this bit of code from the Example class constructor (the values of the variables are set earlier in the constructor):

$this->config = array_merge(array(
    'corePath' => $corePath,
    'chunksPath' => $corePath.'elements/chunks/',
    'modelPath' => $corePath.'model/',
    'processorsPath' => $corePath.'processors/',
    'templatesPath' => $corePath.'templates/',

    'assetsUrl' => $assetsUrl,
    'connector_url' => $assetsUrl . 'connector.php',
    'cssUrl' => $assetsUrl.'css/',
    'jsUrl' => $assetsUrl.'js/',
),$config);

Next, the code loads the JavaScript class file for the JS Example class. This needs to happen before any of the other JS files are loaded because various things are hung off the Example class and it needs to exist. It could go as the first line of each controller, but since it’s likely to be needed in any controller, it makes sense to put it here. The contents of the example.class.js file looks like this:

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

In the next bit of code, a critical step takes place. The config (i.e., $scriptProperties) array is converted to JSON and inserted into the page as part of the JavaScript Ext.onReady function. This allows the JS code to have access to all the properties available in the PHP code.

The next section loads any lexicon files that might be needed by the controller(s), and after that, the code checks for any necessary permissions. The permission check can be performed at various levels. It can go here if every controller and processor will require the same permissions. It can go at the next level down by overriding this function in the individual controller(s), or lower still by using the checkPermissions() method of the modProcessor class.

The getTemplate() method is new here. Usually, when using modExt, the original content of a panel is quite minimal and what the user sees is added by the JavaScript code. For example, the “content” of the main panel of the Example CMP is just this line:

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

The line above is the entire content of the templates/mgr.tpl file. The getTemplateFile() line in the code above just returns the path to that file. MODX processes that file as a Smarty template, but like our example, it can be plain HTML. Because we’re using modExt, the file’s content is quite small, but there’s no reason you couldn’t use an extensive HTML file as your template file. Whatever the template file contains will be displayed by the controller. Note that this has nothing to do with the Templates and Tpl chunks in the Manager’s Element tree.

The Controller File

The concrete class in the file above just tells MODX to call the home controller by default. The default controller’s name is arbitrary, but it’s almost always called home and its file is called home.class.php

Any controllers in the controllers directory will extend the abstract class from the code above (not the concrete class) and override any methods that need different behavior. For example, a particular controller that required different permissions from the other controllers could override the checkPermissions() method. If the controller needed different output, it could override the getTemplateFile() method.

It’s quite common for a CMP to have only one controller, but there can be more. The Login extra, for example, has nine controllers (though at this writing they are the old-style, deprecated .PHP controllers rather than classes).

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 JavaScript loaded by the controller 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.

Most often, the JavaScript on the page calls the connector directly via Ajax and the connector calls the appropriate processor (the Ajax request contains the name of the processor to call). The processor interacts with the database and then returns information to the connector and the connector passes that information, unchanged, back in the form of a response to the Ajax request.

For example, the controller for the “Manage Users” form in the Manager loads JavaScript which displays all the users 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 form fires 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/home.class.php is the default (and only) controller file for the Example CMP. Here is its code:

public function getPageTitle() {
    return $this->modx->lexicon('example');
}

/* Register all the needed javascript and CSS files. */

public function loadCustomCssJs() {
    $this->addJavascript($this->example->config['jsUrl'] . 'widgets/chunk.grid.js');
    $this->addJavascript($this->example->config['jsUrl'] . 'widgets/snippet.grid.js');
    $this->addJavascript($this->example->config['jsUrl'] . 'widgets/home.panel.js');
    $this->addLastJavascript($this->example->config['jsUrl'] . 'sections/home.js');

    $this->addCss($this->example->config['cssUrl'] . 'mgr.css');
}

In the code above, only two methods are overridden. The getPageTitle() method is called by MODX to get the title of the panel being displayed (nothing to do with resource pagetitles). The loadCustomCssJS() method just loads the JS and CSS files necessary for the CMP to function.

The Class file

core/components/example/model/example.class.php (the Example class file). The Example PHP class for the CMP has no methods other than its constructor (though it could if necessary):

function __construct(modX &$modx,array $config = array()) {
        $this->modx =& $modx;
        $corePath = $modx->getOption('example.core_path',null,
            $modx->getOption('core_path').'components/example/');
        $assetsUrl = $modx->getOption('example.assets_url',null,
            $modx->getOption('assets_url').'components/example/');

        $this->config = array_merge(array(
            'corePath' => $corePath,
            'chunksPath' => $corePath.'elements/chunks/',
            'modelPath' => $corePath.'model/',
            'processorsPath' => $corePath.'processors/',

            'assetsUrl' => $assetsUrl,
            'connector_url' => $assetsUrl . 'connector.php',
            'cssUrl' => $assetsUrl.'css/',
            'jsUrl' => $assetsUrl.'js/',
        ),$config);

        $this->modx->addPackage('example',$this->config['modelPath']);
        if ($this->modx->lexicon) {
            $this->modx->lexicon->load('example:default');
        }
    }

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, 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.class.php file’s getTemplateFile() method in the CMP panel in the Manager’s right-hand panel and it can be plain old HTML. At this writing, the SiteCheck CMP has an old-style action file (index.php) 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 described 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 old-style action files after setting $_POST variables and cause serious trouble or penetrate your site. This is the best reason for updating to the new class-based action and controller files. There is no way to execute them since they are simply class files.

For any CMP, it’s important 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 3rd Party Software, 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 14,000 posts.

Comments are closed.