We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
  • Anyone have a succinct explanation of how controllers work in the MODX manager, e.g. for CMPs or for built-in manager actions? I've looked at these for a long time now and I'm still baffled. Why do they need to return a string? And how does that string get automagically mapped to a classname and method name? And why is it so obscure? And which pages in the rtfm docs are most helpful for illuminating this?
    • The action needs a controller. Usually this is set to "index", which translates to:
      File: namespace_core_path/index.class.php
      Class: IndexManagerController

      Typically this index controller extends an abstract class named NameSpaceManagerController which in its initialize() loads up a service class, has a getLanguageTopics() function returning an array of lexicon topics to load and a checkPermissions() method returning a boolean for permissions.

      The index controller would provide a getDefaultController method, which returns for example "mgr/home".

      "mgr/home" then translates to a file in namespace_core_path/controllers/mgr/home.class.php and the class name is expected to be the namespace and each part of the controller ("mgr" and "home") ran through ucfirst, so an action/controller of "mgr/home" would have a class name of NamespaceMgrHomeManagerController. That controller has a process method and a loadCustomCssJs method and a getPageTitle method.

      My controllers don't return a string (processors do though), but the names just follow that naming scheme.

      I'm not sure where in the RTFM this would be, I've been doing it long enough for it to be pretty much second nature...
        Mark Hamstra • Developer spending his days working on Premium Extras and a MODX Site Dashboard with the ability to remotely upgrade MODX and extras to make the MODX world a little better.

        Tweet me @mark_hamstra, check my infrequent blog at markhamstra.com, my slightly more frequent ramblings at MODX.today or see code at Github.
      • Thanks Mark. This really needs to be somewhere in the RTFM, because a structure like this is not 2nd nature to most residents of this planet...

        Care to take a stab at explaining the processors? Why do they return a string? Where are their default paths?
          • 3749
          • 24,544 Posts
          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. wink [ed. note: BobRay last edited this post 10 years, 7 months ago.]
            Did I help you? Buy me a beer
            Get my Book: MODX:The Official Guide
            MODX info for everyone: http://bobsguides.com/modx.html
            My MODX Extras
            Bob's Guides is now hosted at A2 MODX Hosting
          • The connector lives in the assets directory because it has to be available by URL for the Ajax.
            Why not the "connectors" directory?
              Studying MODX in the desert - http://sottwell.com
              Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
              Join the Slack Community - http://modx.org
              • 3749
              • 24,544 Posts
              Good question. I think probably because it's simpler if all component files are either under assets/components or core/components and you only have to set two paths rather than three (the same two that are set for the namespace). AFAIK, there is no MODX component that puts anything in the connectors directory.

              Also, when trying to figure out how an extra works, it's bad enough to have to bounce back and forth between the assets/ and core/ directories without having a third one to bounce to. wink

              BTW, to be more precise, I should have said "*below* the assets directory."
                Did I help you? Buy me a beer
                Get my Book: MODX:The Official Guide
                MODX info for everyone: http://bobsguides.com/modx.html
                My MODX Extras
                Bob's Guides is now hosted at A2 MODX Hosting
              • Hm. Yes, I suppose that makes sense..."user" space is core/components and assets/components. Core/components for the component's core files which will all be included via file system path, and assets/components for files that will be accessed via URL.
                  Studying MODX in the desert - http://sottwell.com
                  Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
                  Join the Slack Community - http://modx.org
                • FWIW, in 2.3, the large number of connector files are going away, in favor of a single, reusable connector.