In this series of articles, we'll look at an easier way to create a Custom Manager Page (CMP). In this article, we'll get a start on creating a minimal CMP, in the next one, we'll modify it so it actually does something.

Custom Manager Pages
A Custom Manager Page (CMP) is a page you add to the MODX Manager. Unlike a widget, which must be lodged in a dashboard, a CMP is a regular Manager page, like the Create/Edit Resource panel or the Manage Users grid. CMP's are launched the same way other Manager pages are — by clicking on a menu choice in the Manager's Top Menu.
One upon a time, CMPs were a lot of work to create. They involved creating menu items, creating actions that launched controllers, linking the menu items to the actions, and putting the whole thing together. Creating a transport package to share your CMP was even more difficult, since the IDs of the actions could only be found during the package installation. Menu items and actions had to be created separately and then linked in a resolver that ran at the end of the package installation process.
CMPs could contain HTML code, but allowed no MODX tags or placeholders to be used.
Much of that process has changed since the old days. We now have class-based controllers and processors. These new controllers and processors handle a lot of housekeeping tasks (like checking permissions and loading lexicon files). Actions are specified directly by name in Menu items, and the old modx_actions
table is no longer used.
As of MODX 2.5.0, thanks to Romain Tripault (Melting Media), we now have a controller class that allows the use of MODX tags and placeholders.
Let's Build a CMP
In this series of articles, we'll build a working CMP. It's still a little more complicated than writing a snippet, but the tools available now make it seem like child's play compared to the old days.
Our use case is a simple search function to find resources that match a search word or phrase. Of course the Manager already has one of these, but it's a case that's easy to understand , and the CMP we create could easily be modified to perform any number of useful tasks. By the end of the following article, you should have a working Search CMP that provides links to edit matching resources.
To build our Search CMP (let's call it MySearch
) we only need three things. A namespace, a controller file, and a menu item. Since we can now use placeholders, we'll add a Tpl chunk to hold the HTML content the controller will display (i.e., the search form). We could put that content in the controller itself, but since it's HTML, it will be easier to write and edit in a Tpl chunk.
Creating the Namespace
The function of the namespace is to hold the two base paths to the CMP's files. This will include a core path and an assets path. That way, when we specify the namespace for our menu item, MODX will know where to look for the CMP's files.
Go to System (gear icon) -> Namespaces. Click on the "Create New" button and enter the following values in the three fields exactly as written:
Name: mysearch Core Path: {core_path}components/mysearch/ Assets Path: {assets_path}components/mysearch/
MODX will replace the two parts in curly brackets with the actual core and assets paths. If you move or rename your core directory later, these paths will still work. Make sure that your paths end in a slash and that there's *no* slash after the closing curly brace.
Click on the "Save" button.
The two paths above specify the standard location, but the files can actually go anywhere. In my development environment, for example, all my extras are under the assets//mycomponents
directory. So, for example, my Core Path for the mysearch namespace is {assets_path}mycomponents/mysearch/core/components/mysearch/
Creating the Menu Item
Now that we have a namespace, we can create our menu item. Go to System (gear icon) -> Menus. Click on the "Create Menu" button. Use the following values for the form fields:
Parent: Top Navigation (or 0) Lexicon Key: My Search Action: home Namespace: mysearch Permission: edit_document,edit_chunk,edit_plugin,edit_snippet,edit_template,edit_user
Leave the other fields blank. Click on the "Save" button. You should see the menu item in the menu tree. If not, reload the page. It should automatically go at the end of the Top Navigation section, but you can click and drag it to another location
If you reload the page (you might have to clear the cache), you should see your menu item in the Top Menu. If not, you probably don't have one of the permissions listed in the Permission field.
If you click on the menu item, you should get a message telling you that MODX couldn't find the controller file, and a similar message in the Error Log. Take a close look at it and make sure there are no doubled or missing slashes. If it's wrong, you can go to System -> Namespaces, right-click on the "mysearch" namespace, and update it.
Creating the Controller File
This file (home.class.php
) goes in the following location:
core components mysearch controllers home.class.php
If you've moved or renamed the core, the first part of the path will be different, but since you used {core_path}
in the namespace Core Path, MODX will know where to look for it, even if you move and/or rename the core in the future.
MODX is actually not all that fussy about where the file is. It has to go beneath the mysearch
directory, but you can put it just under the mysearch
directory, under mysearch/controllers/
, or in mysearch/controllers/default
. If you put it in the default directory, MODX may only find it if you are using the default
theme for the manager. If you're using another theme, you'll need to put it under a directory named for that theme. If you put it where I've suggested above, it will always be found, but if you're using another Manager theme, it may not be styled according to that theme.
In case you're curious, the URL MODX uses to get to the controller is:
http://yoursite.com/manager/?a=home&namespace=mysearch
The a
is the action. MODX uses the namespace core path to get to the component's directory, then looks under it for a file named after the action, home.class.php
or the old-style controller, home.php
. If you ever need to create a link to a controller, you can use the format of the URL above.
Here's the content for the home.class.php
controller file:
<?php /** * mysearch Home Controller */ class mysearchHomeManagerController extends modParsedManagerController { public function getPageTitle() { return 'My Search'; } public function process(array $scriptProperties = array()) { $this->modx::sanitize($_POST); if (isset($_POST) && ! empty($_POST)) { /* For now, just show the $_POST array */ $results = '<pre>' . print_r($_POST, true) . '</pre>'; $this->modx->setPlaceholder('mysearch_results', $results ); } /* Get the form */ $output = '[[!$MySearchTpl]]'; return $output; } }
Unlike the location, MODX is very picky about the class name of the controller. Remember than when we created the My Search menu item, we put home
in the Action field. The format for controller names is:
NamespaceActionManagerController
You have to replace the Namespace and Action parts of that path with the actual namespace and default action (in this case home
).
If you create other controllers, you need to use that exact format for the class name. If you don't, MODX will give you a confusing error message saying that the controller file wasn't found. If the class name of the controller is wrong, MODX thinks it has loaded the wrong file and refuses to go any further. The class name should always be followed by extends modParsedManagerController
, which is Romain Tripault's excellent controller class that handles MODX tags and placeholder. As you can see, we've set a placeholder and used a chunk tag in our controller.
We've set the placeholder mysearch_results
. For now, all it puts there is what's in the the $_POST
array, wrapped in pre
tags to make it easier to read. I always do this when developing form handling code. It's tremendously helpful when writing the code to see exactly what the form puts in the $_POST
array when the form is submitted. On launching the CMP, it will just show array()
, because the $_POST
array will be empty. After you fill out the form and submit it, it will show name
field of each non-empty input, followed by its value.
We've set the $output
variable to a chunk tag and returned it. It will be displayed as the content of the CMP. We could also have just done this:
return $modx->getChunk('MySearchTpl')
Using getChunk()
would be a tiny bit faster, but I wanted to show that you can return tags to be included in the output (including snippet tags). I used the exclamation point to make the chunk tag uncached. I do this during development when the chunk is changing often. The exclamation point can be removed once everything is working to make the CMP page load faster.
If you click on the My Search menu option, you won't see much at this point, because we haven't created the Tpl chunk yet. That's next.
Creating the Tpl Chunk
Create a chunk called MySearchTpl
with this content:
<div class="container"> <h2 class="modx-page-header">[[+ph._pagetitle]]</h2> <div class="x-panel-body shadowbox"> <div class="panel-desc">Search Resources</div> <div class="x-panel main-wrapper"> <form action="#" id="mysearch_form" method="post"> <fieldset id="mysearch_fieldset" style="padding: 50px;"> <div> <label for="search_term">Search For: </label> <input required class="x-form-text x-form-field" size=100 type="text" id="search_term" name="search_term"> </div> <br class="clear"/> <br class="clear"> <div class="mysearch_submit"> <input style="padding:5px;" type="submit" id="mysearch_submit" name="mysearch_submit" value="Launch Search"/> </div> <div id="mysearch_results"> [[+mysearch_results]] </div> </fieldset> </form> </div> </div> </div>
The pagetitle
placeholder is set automatically by the controller class. The mysearch_results
placeholder is set by our own code in the controller file. I've used some of the Manager's styling classes to make things match the Manager style. There are more places Manager style classes could be inserted, but it's not too bad as is.
At this point, you should be able to click on the My Search menu item and see the form. Filling in the Search Term input and submitting the form won't search yet, but you should see the $_POST
fields set by the form.
Coming Up
In the following article, we'll get our CMP to do an actual search.
Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at Hosting.com (formerly A2 Hosting). (More information in the box below.)
Comments (6)
Cherie Brush — Oct 16, 2019 at 04:51 PM
Has anything changed from this to v2.7.2?
Cherie Brush — Oct 16, 2019 at 05:51 PM
Maybe I'll wait for your answer on that. I got the not found error before I uploaded the controller file, but once I did, I'm getting the famous blank white manager screen.
Bob Ray — Oct 17, 2019 at 12:55 AM
I'm not aware of anything that's changed that would affect this. The blank Manager usually means a syntax error in the code. Most likely in a controller or processor.
Sometimes you can track that by looking at Chrome or FF's Dev. tools (Ctrl-Shift-i) and watching the Network tab as you trigger the crash. See this page for more info: https://bobsguides.com/blog.html/2013/05/22/debugging-cmps-a-horror-story/
Cherie Brush — Oct 17, 2019 at 05:16 PM
Yeah, I've been looking at that page. I feel I might look at it a lot. :-)
It died as soon as I uploaded the controller file. I even put a debug/return right at the top of the process function and it still didn't work. Nothing in the logs. I think for now I'll just get started with MyComponent. I can probably compare the files it dumps out for me.
Hugo Peek — Sep 16, 2020 at 03:44 AM
I also got a blank screen and found an error in the Nginx logs saying:
Declaration of mysearchHomeManagerController::process() must be compatible with modExtraManagerController::process(array $scriptProperties = Array)
To fix this, change the first line of the process function to:
Thanks again for a great write-up Bob. It's really helpful, especially with that new modParsedManagerController class.
Bob Ray — Sep 17, 2020 at 12:05 AM
Thanks Hugo for the correction and the kind words. I've made that change to the
process()
method in the code above.Please login to comment.