Let me add my two cents here. It's not necessarily true, but it helps me to think of the controllers as managing the display. Whatever they return is displayed in the Manager's CMP panel.
Let me walk through the structure and abbreviated code of the Example CMP included with MyComponent. I based it on a the structure of a number of different MODX extras. It may help to see the structure and some code in one place rather than wandering back and forth through the files.
This first file is what is executed when the user selects the Example CMP menu choice under "Components" in the Manager's Top Menu. Its name is set in Manager | Actions, and its location is determined by the path set in the Example namespace: {core_path}components/example/.
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;
core/components/example/controllers/index.php (the main controller file -- it instantiates the Example class and returns what comes back from its initialize method, which returns what comes back from handleRequest()):
require_once dirname(dirname(__FILE__)).'/model/example/example.class.php';
$example = new Example($modx);
return $example->initialize('mgr');
core/components/example/model/example.class.php (the Example class file's constructor and initialize() method):
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. Since no user action has occurred yet,
the 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;
}
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;
}
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):
$modx->regClientCSS($example->config['cssUrl'].'mgr.css');
$modx->regClientStartupScript($example->config['jsUrl'].'example.js');
/* Make the config settings available to the JS code */
$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 is "included" it doesn't need to return anything */
return '';
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;
The upshot is that all the CSS and JS is loaded, config is set up, necessary classes are loaded and initialized and all that's actually returned is:
'<div id="example-panel-home-div"></div>'
The JS code that's been loaded will replace that with the appropriate JS widgets, which put buttons and links and context menus on the page. When they are selected by the user, they either modify what's there, or fire an Ajax request. None of the above actually *does* anything except set up the display the user sees.
The real action is performed by the processors, which are called with Ajax in the JS (typically via a connector file). The connector lives below the assets directory because it has to be available by URL for the Ajax. It 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. */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/config.core.php';
require_once MODX_CORE_PATH . 'config/' . MODX_CONFIG_KEY . '.inc.php';
require_once MODX_CONNECTORS_PATH . 'index.php'; /* gets and sanitizes the actual $request */
$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);
$modx->lexicon->load('example:default');
/* handle request */
$path = $modx->getOption('processorsPath', $modx->example->config, $exampleCorePath . 'processors/');
$modx->request->handleRequest(array(
'processors_path' => $path,
'location' => '',
));
The final part of the code 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.
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 just the process() method, and have it return something.
BTW, there are two flavors of processors, class-based and procedural. When executing a processor request, MODX will look for a class file. If it finds it, it will call its initialize() and process() methods and return what comes back from process(). If it doesn't find a class, it will look for a regular .php file 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 something to the JS Ajax request.
None of this is strictly necessary. I'm working on a CMP that has an index.php file that works like a regular form-processing snippet. It instantiates the $modx object and its own class, loads the CSS and Lexicon, displays a form, responds to the $_POST, and prints output below the form - no ExtJS, no processors, no controllers, and no connectors. It 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 above with modExt provides many powerful UI options that would be *very* difficult to duplicate on your own.
The structure I describe above seems unnecessarily arcane and labyrinthine to me too, but there may be reasons why it needs to be that way that I don't understand. Sometimes, though, I suspect that someone had a professor who really liked abstracting everything and lowered grades if too many files were longer than a few lines.
[ed. note: BobRay last edited this post 10 years, 7 months ago.]