Exploring MODx From the Inside: Creating Snippets

Writing Custom Snippets

Writing snippet code is much the same as writing any PHP code. There are, however, a few differences from using the customary <?php ... ?> tags in HTML code, as well as some suggestions which, while not strictly required, do make your snippets easier and more flexible to use. This chapter is not intended to be a PHP tutorial; it is presumed that you are at least somewhat familiar with basic PHP programming. A good resource to have in your browser bookmarks is the PHP documentation, and a couple of good sources for learning PHP are the Diving Into PHP videos and the Practical PHP Programming free online book.

Snippets are for PHP code, although they can also be HTML code with embedded PHP tags. Since snippets are processed by the MODx parser, this code is stored in the database, and the parser passes it to the PHP eval() function, with the snippet's parameters passed as parameters in that function. Any PHP code that is stored in external .php files is included with include or require directives, just as in any other PHP code.

Documentation

While extensive PHPDoc style comments are not required, they are good practice, especially if the snippet is more than just a few lines. Most simple snippets have minimal if any commenting, but at least a leading comment with some basic usage is a good idea if you are intending to share your snippet with the MODx community. Whether you comment your snippet or not, an external README.txt or INSTALL.txt file should be provided. More extensive snippets, such as eForm and WebLoginPE, come with excellent documentation in a docs directory included in their archive. Do be sure to include a usage example or two in your documentation.

Inline documentation of your code is also a good practice. MODx is very much a community project in the best tradition of Open Source, so making it easy for the community to help with extending and customizing your snippet, as well as bug-fixing, would be in the true spirit of MODx.

Code

Your snippet must be enclosed in <?php ... ?> tags in the same way any .php file is. MODx will include these tags for you when saving the snippet if they aren't there. Take care when copying snippets from other sources if they already contain the tags, however, since MODx will not remove redundant tags, and the redundant tags will result in a PHP error when your snippet is processed. So make sure that when pasting entire snippets that the original PHP tags in the editing field are removed.

Create Snippet

Snippets are processed through the PHP eval() function, so like any function they should end with a return statement. Any output to be displayed should be collected into a variable, and that variable returned. Echo and print directives and functions can also be used, but the location of their output may not always be as expected.

Gotcha - one thing to keep in mind is that as in any other PHP code, variable scope applies. So if your snippet has functions, and you want to use the $modx class variable for making use of the MODx API, you have to declare $modx as global in your function:

function myFunction() {
    global $modx;
    $pageID = $modx->documentIdentifier;
    $table = $modx->getFullTableName('site_content');
    $rs = $modx->db->select('*',$table,'id = ' . $pageID);
    ...
}

Including External Files

To keep the snippet itself as small and simple as is practical, most snippets more than just a few lines are stored in ordinary .php files in the assets/snippets/ directory. The code in the snippet in the Manager can be reduced to basic configuration, and the external files are included as they would be in any other PHP script.

Here is an example of how the Jot snippet sets up a nice, proper OO initialization.

$jotPath = $modx->config['base_path'] . 'assets/snippets/jot/';
include_once($jotPath.'jot.class.inc.php');

$Jot = new CJot;
$Jot->VersionCheck("1.1.4");
$Jot->Set("path",$jotPath);
...
return $Jot->Run();

Now, let me say at this point that I just don't like OOP. It's a personal thing. So if you want to study MODx with OOP and MVC and ORM and CRUD, hop on over to the MODx Revolution side and have a fine time with it. Here you will be shown how to plod along with simple, old-fashioned modular code.

$var1 = isset($var1) ? $var1 : 'value1';
$var2 = isset($var2) ? $var2 : 'value2';
include $modx->config['base_path'] . 'assets/snippets/mysnippet/mysnippet.inc.php';

And that's it. The main code for the snippet's function is in the mysnippet.inc.php file, which properly returns itself. This file can be a mixed HTML and PHP file like any .php file if you like, or it can be pure PHP code, collecting any output into a variable and returning it at the end.

Snippet Parameters

There are several ways of passing parameters to your snippet. Two are built-in to snippet functionality with no intervention on the developer's part, while the third requires that an external file is included or required.

Global parameters can be set in the snippet's Properties tab. These will apply to all snippet calls. They are entered into the Default Properties field in this form:

&varname=Label;type;value;default &var2name=Label;type;value;default

Each parameter begins with an ampersand and the variable name, followed by an equal sign, then the label for the property field, the type (text, list, etc), the value(s), and the default value all separated by semicolons. When using the "list" type, the values are a comma-separated list of values, and the default value can be specified. Once the parameters are all entered, clicking outside the field will display a new section with rows of labels and form fields for easily editing each parameter. List types will have a select list, with the default value selected.

Snippet Properties

Not very many snippet use this method of setting parameters, since they are global. Most often snippets are called with the desired parameters set in the individual snippet tags. The snippet tags take the form of

snippet-name? &param1=`value1` &param2=`value2`

Since the ?, & and = are used as parameter delimiters, they cannot be used in the values.

Both the global parameters from the Properties tab and parameters set in the snippet tags are passed as parameters to the eval() function when snippets are evaluated, and are available to the snippet code as normal variables.

A third way of handling parameters is to use an external config file. A single parameter in the snippet tags is required to pass the name of the desired config file. A classic situation here might be to use the Properties tab to set the path to the directory where external files for the snippet are stored (such as assets/snippets/MySnippetName), while a parameter in the snippet tags would specify which config file to use for that particular invocation of the snippet. A snippet such as Wayfinder may be called several times on a single page, each with its own config file to generate several different menus and lists.

The snippet code must include or require the config file at the beginning of its code in order to make the variables in the config file available to the entire snippet.

$config = isset($config) ? $config : "default";
$cfile = $path . "configs" . $config . ".config.php";
if(is_file($cfile)) {
    include $cfile;
}

The config file is a normal .php file, where the desired parameters are simply normal PHP variable definitions. Since it's a normal .php file, you can have conditional or otherwise dynamically created values for the variables.

Let's say you want your manager users to be able to view or edit unpublished pages, but only the manager users. You can use Ditto's showPublishedOnly option set to 0 to list all documents, including unpublished ones. But you don't want to show the unpublished documents in the list for non-Manager users, since they can't view unpublished pages anyway. A conditional variable definition would be useful here.

// default is 1 = list only published resources
if($_SESSION['managerValidated']) $showPublishedOnly = 0;

Now your logged-in Manager users will have a list of all resources, published and unpublished, and since he can view unpublished pages he can use the QuickManager to edit them.

A few other advantages to coding your snippets to allow external config files are that it avoids long, ugly parameter lists in the snippet tags, and it also avoids the problem of TinyMCE and other Rich Text editors mangling the parameters of snippet tags. Besides, it's the MODx way to allow as may ways as possible to accomplish something.

Your snippet code should always check for the presence of each possible parameter, setting a default value if no custom value is found. If for some reason a default value is not desired, but a value is required and the snippet cannot simply ignore the lack of a value, then a suitable error message should be returned. When a return statement is encountered in snippet code, the snippet exits at that point. You might prefer to have a conditional that will return an error message only if a logged-in manager user is viewing the page, while the snippet will simply quietly fail if it is viewed by non-manager visitors.

$value = isset($value) ? $value : "default";
if(!isset($required)) { 
    if($_SESSION['managerValidated']) { 
        return "Required value not provided"; 
    } else { 
        return; 
    }
}

Yet another often overlooked way of assigning parameters to a snippet is to share the properties from a module. A module is used to add features to the Manager, and like snippets and plugins they can be given global properties in their Properties tab. A module might be used to allow management of custom database fields, such as in a gallery or a calendar application. A snippet would be used to display the gallery or the calendar in the front-end. Properties such as the location of files to be included or the formatting of date/time output set in the module's property list can be shared with the snippet, so you don't have to deal with them in both locations. If a module is configured to share its properties and added the snippet to its dependancy list in its Dependency tab, then the module will appear in the Import Module shared parameters select drop-down.

Shared Parameters

Using Placeholders

In many cases, the output of parts or all of a snippet's process need to be displayed in various locations, or with customized structures. For example, the Wayfinder snippet by default generates a plain nested unordered list. While that is a common way to handle a menu structure, there are many cases where a different structure is desired. Wayfinder handles this by generating several data items, loading them into the MODx placeholders array, and allowing for mini-templates to determine the structure in which these items will be displayed. Placeholder tags in these mini-templates are replaced by the data from the placeholders array.

Some templates manage their placeholder data themselves, using the str_replace() function and their own arrays of data. Others use the MODx placeholders array and explicitly call the $modx->mergePlaceholders() API function. However, this only applies to placeholders used by the snippet in mini-templates for structuring their output. If your placeholder needs to be available within the context of resource content or the main template, you must use the $modx->setPlaceholder() function and let the MODx parser handle the merging of the document and the placeholder's value.

Using Events

Events are places in code where a "hook" is placed to allow custom code to be inserted at that point. It is a way to allow customization of a snippet's behavior without modifying the snippet's core code. MODx uses an event model in its document parser, and plugins are intended to "listen" for these events and insert their code into the process.

There are a number of ways that snippets can implement events at key points during their processing. One is to add their events to the database in the "system_eventnames" table. There are three fields:

When a snippet adds its events to the database, the events become available to plugins, and can be accessed like all of the other events available in the base MODx system. A few snippets that make use of this method are the WebLoginPE snippet and the TreasureChest snippet.

// Create the additional MODx events if they do not exist
$system_eventnames = $modx->getFullTableName('system_eventnames');
$newEvents = array('OnBeforeWebSaveUser', 'OnBeforeAddToGroup', 'OnViewUserProfile');
foreach ($newEvents as $aNewEvent){
    $findEvent = $modx->db->query("SELECT * FROM ".$system_eventnames." WHERE `name` = '".$aNewEvent."'");
    $limit = $modx->db->getRecordCount($findEvent);
    if ($limit == 0) {
        $addEvent = $modx->db->query("INSERT INTO ".$system_eventnames." (`name`,`service`) VALUES ('".$aNewEvent."', 3)");
    }
}

Such a snippet uses the MODx API function 'invokeEvent()' to make use of plugin code:

function OnWebLogin()
{
    global $modx;
    $parameters = array('user' => $this->User);
    $modx->invokeEvent('OnWebLogin', $parameters);
}

Another way for a snippet to implement an event model is to use custom snippets with functions, and when the snippet code reaches the event "hook" in its processing it checks for the existence of the specified function and calls it. The eForm snippet uses this method to allow modifications to the snippet behavior to be customized at various points. A user specifies the name of a function to use at the desired event, and provides that function in another snippet called before the eForm snippet is called so the function is available.

	# invoke onBeforeFormParse event set by another script
if ($eFormOnBeforeFormParse) {
	if( $isDebug && !function_exists($eFormOnBeforeFormParse))
		$fields['debug'] .= "eFormOnBeforeFormParse event: Could not find the function " . $eFormOnBeforeFormParse;
	else{
		$templates = array('tpl'=>$tpl,'report'=>$report,'thankyou'=>$thankyou,'autotext'=>$autotext);
		if( $eFormOnBeforeFormParse($fields,$templates)===false )
			return "";
		elseif(is_array($templates))
			extract($templates); // extract back into original variables
	}
}

Comment On This Article

Do you find something unclear? Did I miss something or get something wrong? What do you like or not like about this chapter? Please do not ask for help here; use the forums if you need help.

If you have trouble reading the code, click on the code itself to generate a new random code.