Easy MODX Custom Manager Pages (CMPs) IV

Adding the controller file


In the last article, we added some checkboxes to the form and some new Tpl chunks. Those won't work without our new controller code, so in this article, we'll look at a bigger, but still fairly simple, controller.

MODX logo

See the previous articles for the Tpl chunks, menu, and namespace information.


New Code in the Controller

Remember that the controller file (home.class.php) is in the core/components/mysearch/controllers" directory. It's that file that we'll be changing.

core
    components
        mysearch
            controllers
                home.class.php


New Controller Code

We've made significant changes to the controller code. Several new methods have been added (more on those in a bit). We've also modified some of the existing sections. One goal, here, is to make the code ready for the creation of a processor that may be called by new JavaScript code in a future version. Even without the JavaScript, much of this code belongs in a processor, rather than a controller. One common-sense way of thinking of this is to consider that in a typical MVC (model, view, controller) design, controllers interact with the user (via the view), while processors interact with the database (model).

To get ready for this, we've created a processor() method (not to be confused with the original process() method that's part of any MODX controller). The goal is to create a processor() function that receives the same information as a real processor would, and returns what a real processor would return. That way, the processor's code (and any functions that support it) can easily be moved out of the controller, and into a real processor.

We've also made some changes to get the code ready for searches for other object like chunks and templates. The theory is that to add a search for chunks, for example, the only thing that would need to change is the form itself, because the controller and processor code would already be set up to process any additional elements added to the form.

In addition, the code that produces the output for a single section — the results of a search for a single object type — has been moved out of the find() function to a function of its own (processCollection()). This is in keeping with the coding principle that a function should have only one job.

Here's new code for the home.class.php file:

<?php

class mysearchHomeManagerController extends modParsedManagerController {

    protected $maxResults = 5;
    protected $results = '';
    // Tpl for each line of results (see initialize())
    protected $resultTpl = '';

    public function getPageTitle() {
        return 'My Search';
    }

    /* Called first automatically by MODX */
    public function initialize() {
        parent::initialize();

        /* Change $maxResults if there's a Setting ?*/
        $maxResults = $this->modx->getOption('ms_max_results',
            null, false, true);
        if ($maxResults) {
            $this->maxResults = $maxResults;
        }

        /* Set result Tpl */
        $this->resultTpl = $this->modx->getChunk('MySearchResultTpl');

        /* Load JavaScript */
        /* $assetsUrl = $this->modx->getOption('mysearch.assets_url',
                null, $this->modx->getOption('assets_url') .
                'components/mysearch/', true);
        $fields = array('connector_url'  => $assetsUrl . 'connector.php' );
        $this->modx->regClientStartupScript(
            $this->modx->getChunk('MySearchJS', $fields), true);
        */
    }

    /** Get objects that match search criteria.
     *  Returns a collection of xPDO objects or
     *  an empty array if there are no matches */
    public function find($searchTerm, $objectType, $fieldsToSearch) {
        /* Note:The 'id'  field has to be handled differently,
            because we want an exact search */
        $criteria = array();
        $query = $this->modx->newQuery($objectType);

        /* Get and remove first field */
        $firstField = (string) array_shift($fieldsToSearch);
        if ($firstField === 'id'){
            $criteria[$firstField . ':=']  = $searchTerm;
        } else {
            $criteria[$firstField . ':LIKE'] = "%" .
                $searchTerm . "%";
        }
        foreach($fieldsToSearch as $field) {
            if ($field === 'id') {
                $criteria['OR:' . $field . ':='] =
                    $searchTerm;
            } else {
                $criteria['OR:' . $field . ':LIKE'] = '%' .
                $searchTerm . '%';
            }
        }

        $query->where($criteria);
        $query->limit($this->maxResults);

        $collection = $this->modx->getCollection($objectType, $query);

        return $collection;
    }

    /** Create output for one search section (object type).
     *  Returns a string with HTML for the section  */
    public function processCollection($collection, $objectType,
        $nameField, $action){

        /** @var $collection array -- other vars are all strings */
        /* All members of $collection match the search term */
        $output = '';
        foreach ($collection as $object) {
            /** @var $object xPDOObject */
            $id = $object->get('id');
            $name = $object->get($nameField);

            /* Get the full output line for one hit */
            $output .= $this->addToResults($action, $name, $id);
        }

        /* If we have results, create the full section code for the
           current object using the MySearchSectionTpl chunk */

        if (!empty($output)) {
            /* Convert 'modResource' to 'Resources' etc.
               for the section header */
            $header = ucfirst(str_replace('mod', '', $objectType)) . 's';

            /* prepare placeholder array for getChunk() */
            $phs = array(
                'mysearch_header' => $header,
                'mysearch_id' => $objectType,
                'mysearch_rows' => $output,
            );

            $output = $this->modx->getChunk('MySearchSectionTpl', $phs);
        }
        return $output;

    }

    /* Create and return one line based on results Tpl chunk */
    public function addToResults($action, $name, $id) {
        /** @var $this->resultTplChunk modChunk */
        $line = str_replace('[[+action]]', $action, $this->resultTpl);
        $line = str_replace('[[+id]]', $id, $line);
        $line = str_replace('[[+name]]', $name, $line);
        return $line;
    }

    /* Main control function called automatically after
     * initialize(). */
    public function process() {
        if (isset($_POST) && !empty($_POST)) {
            $results = $this->processor($_POST);
            if (empty($results)) {
                $results = '<br><br><b>No Results</b>';
            }
            $this->modx->setPlaceholder('mysearch_results', $results);
        }
        $output = '[[!$MySearchTpl]]';

        return $output;
    }

    /* Produces output from $_POST. Will be replaced by
       processor file */
    public function processor($fields) {
        $output = '';

        /* Sanitize $_POST fields */
        $this->modx::sanitize($fields);

        $searchTerm = $this->modx->getOption('search_term',
            $fields, null, true);

        $objects = $fields['search_objects'];

        foreach ($objects as $objectType => $fieldsToSearch) {

            $nameField = $this->getNameField($objectType);
            $action = $this->getAction($objectType);

            /* Get objects that match search */
            $collection = $this->find($searchTerm, $objectType,
                $fieldsToSearch);

            /* If results found, create results display */
            if (!empty($collection)) {
                $output .= $this->processCollection($collection,
                    $objectType, $nameField, $action);
            }
        }

        return $output;
    }

    /* Get field containing the 'name' of the object */
    public function getNameField($objectType) {
        /* set default */
        $nameField = 'name';

        switch($objectType) {
            case 'modResource':
                $nameField = 'pagetitle';
                break;
            case 'modUser':
                $nameField = 'username';
                break;
        }
        return $nameField;
    }

    /* Get correct action for updating object */
    public function getAction($objectType) {
        $action = null;

        /* Remove 'mod' prefix and convert to lowercase */
        $field = strtolower(str_replace('mod', '', $objectType));
        switch($field) {
            case 'resource':
                $action = 'resource/update';
                break;
            case 'user':
                $action = 'security/user/update';
                break;
            default:
                $action = 'element/' . $field . '/update';
                break;
        }

        return $action;
    }
}

The initialize() function is the same except that we now get the result Tpl from a chunk, rather than hard-coding it into the function. There is also new commented-out code for loading JavaScript to process the form. We'll look at that in the next article.

We've created a new method, processor() to simulate an actual processor file that might be called by a JavaScript Ajax call. Our process() method now just hands off the $_POST array to the processor(). The processor() code sanitizes the fields ($_POST), gets the search term, extracts the ['objects'] member, then loops through the objects, calling find() with the appropriate arguments for each object type.

The find() method builds the query criteria dynamically, based on the fields to search passed in to it. Then it queries the database, and passes the results (if any) back to the processor function. If there are results, the processor calls processCollection(), which creates a section of output for each object searched, calling addToResults() for each list item to be added to the results display.

To prepare for the addition of other objects to search, we've added the getNameField() function to get the field that holds the name of the object, since it won't always be pagetitle. We've also added the getAction() function to get the correct action that needs to be called to update (edit) the object because it won't always be resource/update.

Although the find() method will only be called once given our current form, and the $collection variable will only hold results from the search for resources, the code is now designed to process additional objects like chunks and templates as soon as they are added to the form. In fact, if you've been building this as we go along and you've got it working, you should be able to just add a section to the form for chunks with input names like this for the checkboxes:

name="search_objects[modChunk][]"

Here's the code for a chunks section. Add it just below the checkbox div for resources:

<h3 style="margin-top:30px;">Chunks</h3>

    <div id="checkbox_div2" class="x-form-check-wrap">
        <input style="position:static;" type="checkbox"
               id="modChunk_name" autocomplete="off"
               class="x-form-checkbox x-form-field"
               name="search_objects[modChunk][]" value="name">
        <label for="modChunk_name" class="x-form-cb-label"> name</label>
        <br/>

        <input style="position:static;" type="checkbox"
               id="modChunk_description" autocomplete="off"
               class="x-form-checkbox x-form-field"
               name="search_objects[modChunk][]" value="description">
        <label for="modChunk_description" class="x-form-cb-label"> description</label>
        <br/>
        <input style="position:static;" type="checkbox"
               id="modChunk_content" autocomplete="off"
               class="x-form-checkbox x-form-field"
               name="search_objects[modChunk][]" value="content">
        <label for="modChunk_content" class="x-form-cb-label"> content</label>
        <br/>
        <input style="position:static;" type="checkbox"
               id="modChunk_id" autocomplete="off"
               class="x-form-checkbox x-form-field"
               name="search_objects[modChunk][]" value="id">
        <label for="modChunk_id" class="x-form-cb-label"> id</label>
        <br/>

    </div>

Chunks, templates, plugins, and snippets should all work if you add them to the form (and remember to use the appropriate fields). If you add a chunks section, for example, be sure to use chunk fields, not resource fields for the values.


Summing Up

This series, so far, shows how to create a simple MODX CMP that performs a useful task. The only drawback of our CMP is that it reloads the entire Manager page and clears all your search fields every time you do a search. It's possible to avoid the reload with an iFrame, but the more traditional way is by using JavaScript and Ajax. We'll do that in the next article.


Coming Up

In the next article, we'll move the processor() function and all the functions it depends on into a processor file and use a JavaScript Ajax call to send the same array (in JSON format) to that processor. Our controller will be much shorter than it is above.




Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at A2 hosting. (More information in the box below.)



Comments (0)


Please login to comment.

  (Login)