Module Tutorial

This tutorial walks you through how to create a new module for CMS Made Simple (referred to as "CMSMS" from here on out).

Pre-Requisites

This tutorial assumes you are already familiar with:

  • PHP programming
  • Basic object-oriented programming concepts (encapsulation, inheritance, etc.)
  • MySQL (or databases in general) -- tables, fields, data types, SQL syntax, etc. Also, you should know how to use some kind of database administration tool (such as PhpMyAdmin) for examining the CMSMS database.
  • How CMSMS works from an end-user's perspective -- you know what templates, stylesheets, pages, and content are, you know that permissions can be set for various functionality, and you know that including {tags} in templates will display module content.


No existing knowledge of how CMSMS modules work is required -- that's what this tutorial is for!

What Are We Building?

We are going to build a very basic product catalog module. I want administrative users to be able to add product information to the database via the admin panel, and then create a content page that will display all products in the system. Eventually, I would like to allow admin users to be able to choose different display options (number of columns per page, etc.), to allow products to be categorized by type or manufacturer (so different pages will display different sets of products), allow for pagination (so you could specify to show a certain number of products per page), allow product images to be uploaded and displayed, and allow the product information to be searchable. But for the purposes of this tutorial, we will keep things simple -- product information will be limited to a name, a description, and a price, and all products will be displayed in a single list on a single page. We will call this module "Catlist" (an intentionally dumb name, so as to avoid confusion with the existing, better-named Cataloger module).

Structure of a CMSMS Module

It can be quite confusing to figure out how the structure of a module works -- what code goes where, which files are critical, etc. First of all, everything needed for your module lives inside a directory whose name is the name of your module. Since the name of our module is "Catlist", our module directory will be called "Catlist". This directory lives inside the "Modules" directory of the CMSMS installation. If you take a look at some of the existing modules (for example, the Skeleton module), you will see a lot of files and sub-directories, which can roughly be broken down as follows:

  • action.___.php files: These files contain code that handle various "actions" that are called by CMSMS -- for example, when the admin panel needs to check certain permissions, or new content is saved to the database, these files can be called. As with the "method.___.php" files below, these files are not required -- all code in these files can also be put into the "ModuleName.module.php" file. So, for the purposes of this tutorial, we will not use any "action.___.php" files, but bear in mind that eventually you will want to separate out some code into them so as to keep the module more maintanable.
  • "images" directory: Any images that are displayed for your module (either in the admin panel you will set up to allow administrators to change content and settings, or in the front-end when content is displayed to end-users) will be placed here.
  • index.html file: This is always just a blank file. In the event that the address for this directory is typed directly into the browser address bar, having this file here will prevent the display of the file listing (which could be a security concern).
  • "lang" directory: In order to support multiple languages, you can put files in this directory (one file per language), and then in your module code, instead of writing out strings of text, you can call the function "$this->Lang('string_name')" to retrieve the text in the appropriate language.
  • method.___.php files: These files contain code that handle various "methods" that are called by CMSMS -- normally this is for installing, upgrading, and uninstalling the module. As with the "action.___.php" files above, these files are not required -- all code in these files can also be put into the "ModuleName.module.php" file. So, for the purposes of this tutorial, we will not use any "method.___.php" files, but bear in mind that eventually you will want to separate out some code into them so as to keep the module more maintanable.
  • ModuleName.module.php file: This file is the heart of your module -- it is where all the action is! All code that handles installing and uninstalling our module, allowing the administrator to add products to the database, and displaying products on the front-end site will be in this file.
  • "templates" directory: Any templates that are used to display content for your module live in this directory -- these templates are both for displaying your admin panel, as well as displaying content on the front-end site.

How CMSMS Displays Module Content

The most difficult concept for me to grasp at first was figuring out how CMSMS actually displays module content on the front-end site. I am not sure I understand it completely, but by looking at existing modules it seems that there are two ways to achieve this:

  1. Inclusion of a {module} tag within a page: The administrative user creates a new page of the generic "content" type, and includes a tag (such as "{cms_module module='Catlist'}"), which has the effect of calling our module's DoAction function from our Catlist.module.php file, at which point we respond to it by retrieveing data from the database and putting that data into one of our smarty templates, which is subsequently included on the page.
  2. Creation of a new content type: New content types will be added to the drop-down list of the "Edit Page" admin panel. When this content type is selected, the admin user is shown a form with which they can enter the various parameters for your module. These parameters correspond to the parameters that you would make available in your {module} tag. When the page is saved with our content type, it winds up having the same effect as if the user set the content type to "content" and then added our {module} tag to the content text area (except it was done with an easy-to-use form and didn't require them to remember the various parameter names of your module). Author's note: I am unsure of the details on how new content types work -- perhaps someone can explain it better?

For the purposes of this tutorial, we will use method 1 -- including a {module} tag in a "generic content" page.

Getting Started: Creating The Module Directory

Okay, we are finally ready to create our module! Start out by navigating to the "Modules" directory of your CMSMS installation. Create a new directory called "Catlist". Pay attention to the spelling and capitalization -- it is important that when we refer to our module in code that it is the same exact name. Now fire up your text editor of choice, and create a new file called "Catlist.module.php" (again, pay attention to the spelling and capitalization). We'll start off with just the basics -- paste the following code (which I lifted from the Skeleton module) into the file now:

<?php
/* Your initial Class declaration. This file's name must
   be "[class's name].module.php", or, in this case,
   Catlist.module.php
*/ 
class Catlist extends CMSModule
{

    /*---------------------------------------------------------
       GetName()
       must return the exact class name of the module.
       If these do not match, bad things happen.

       This is the name that's shown in the main Modules
       page in the Admin.
      ---------------------------------------------------------*/
    function GetName()
    {
        return 'Catlist';
    }

    /*---------------------------------------------------------
       GetFriendlyName()
       This can return any string.
       This is the name that's shown in the Admin Menus and section pages
          (if the module has an admin component).
      ---------------------------------------------------------*/
    function GetFriendlyName()
    {
        return 'Simple Catalog Product List';
    }


    /*---------------------------------------------------------
       GetVersion()
       This can return any string, preferably a number or
       something that makes sense for designating a version.
       The CMS will use this to identify whether or not
       the installed version of the module is current, and
       the module will use it to figure out how to upgrade
       itself if requested.
      ---------------------------------------------------------*/
    function GetVersion()
    {
        return '0.1';
    }
         
         
    /*---------------------------------------------------------
         IsPluginModule()
         This function returns true or false, depending upon
         whether users can include the module in a page or
         template using a smarty tag of the form
         {cms_module module='Skeleton' param1=val param2=val...}
         If your module does not get included in pages or
         templates, return "false" here.
    
         (Required if you want to use the method DoAction later.)
         ---------------------------------------------------------*/
    function IsPluginModule()
    {
         return true;
    }
}
?>

Okay, it's not much, but it's a start. The code comments explain what each function does (although it should be fairly obvious). If you haven't done so already, save this file under the "Catlist" directory we just created, and call it "Catlist.module.php".

Now, let's take this opporunity to learn about the multi-language feature of CMSMS. As explained above, instead of putting literal text strings in your code, you can use the "$this->Lang()" function to pull the appropriate text from a separate language file. Let's create the structure for that now. Create a new directory inside the existing "Catlist" direcory called "lang". Go back to your text editor and create a new file in the "lang" directory called "en_US.php". So far, the only string we want to put in this file is the "Friendly Name" (because the actual name is the same regardless of the language -- kind of like how "Coke" is still called "Coke" in Japan [I think]). Add this code to the new file:

<?php
        $lang['friendlyname'] = 'Simple Catalog Product List';
?>

Now go back to the "Catlist.module.php" file, and change the line in the "GetFriendlyName()" function to this:

        return $this->Lang('friendlyname');

If you or someone else wanted to add a new language to the module, they could create a new file in the "lang" folder (for example, "fr_FR.php" for french) and change the value of $lang['friendlyname'] to something like:

<?php
        $lang['friendlyname'] = 'Catalogue Simple des Produits';
?>

Writing Installation/Uninstallation Code

Most modules will require additions to the CMSMS database (so that we can save and retrieve our data), new permission roles (so admins can allow or disallow users from performing certain operations with it), and some preferences (so admins can change various settings). These things must be done upon installation of the module, and if we put them in a function called "Install()", they will get called when the "install" link is clicked from the admin panel. For the purposes of this module, we will create a single database table to store product information, and a single permission role that allows (or disallows) users from adding/editing/removing products from the catalog. We will not have any preferences.

First, add this function to the Catlist.module.php file:

function Install()
{
}

The database table will be very simple -- we will have fields for product ID, product name, product description, and price. CMSMS uses the ADODB library to interact with the database (see the ADODB manual for more details). Enter the following code into the Catlist.module.php file (within the curly braces of the Catlist class):

function Install()
{
    //Get a reference to the database
    $db = $this->cms->db;

    // mysql-specific, but ignored by other database
    $taboptarray = array('mysql' => 'TYPE=MyISAM');

    //Make a new "dictionary" (ADODB-speak for a table)
    $dict = NewDataDictionary($db);

    //Add the fields as a comma-separated string.
    // See the ADODB manual for a list of available field types.
    //In our case, the id is an integer, the name is a varchar(100) field,
    // the description is a text field, and the price is a float.
    $flds = "id I KEY,
             name C(100),
             description X,
             price F";

       //Tell ADODB to create the table called "module_catlist_products", 
       // using the our field descriptions from above.
       //Note the naming scheme that should be followed when adding tables to the database,
       // so as to make it easy to recognize who the table belongs to, and to avoid conflict with other modules.
       $sqlarray = $dict->CreateTableSQL(cms_db_prefix().'module_catlist_products', $flds, $taboptarray);
       $dict->ExecuteSQLArray($sqlarray);

       //Now create a "sequence", which is used internally by CMSMS and ADODB
       // to increment our id value each time a new record is inserted into the table.
       $db->CreateSequence(cms_db_prefix().'module_catlist_products_seq');
}

Now add one more line to the end of the function that will create our permission role in the system:

function Install()
{

/* ... all the database code from above... */

//Create a permission
//The first argument is the name for the permission that will be used by the system.
//The second argument is a more detailed explanation of the permission.
$this->CreatePermission('Catlist Admin', 'Manage Catlist');
}

Also add this function (within the curly braces of the Catlist class), which will be displayed to the admin user upon a successful installation

function InstallPostMessage()
{
    return $this->Lang('postinstall');
}

And of course we need to add the english language version of this message to our "lang" file:

$lang['postinstall'] = 'Catlist successfully installed!';

Since installing our module required us to add things to the CMSMS system, we will need to remove those things when the module is un-installed (yes, I know the thought of that is upsetting, but let's try to be good programming citizens here and set an example for the youth). Not surprisingly, this is done in a function called "Uninstall()". Basically, we need to un-do anything we did in the "Install()" function. Add this code to the Catlist.module.php file (within the curly braces of the Catlist class):

function Uninstall()
{
    //Get a reference to the database
    $db = $this->cms->db;

    //Remove the database table
    $dict = NewDataDictionary( $db );
    $sqlarray = $dict->DropTableSQL( cms_db_prefix().'module_catlist_products' );
    $dict->ExecuteSQLArray($sqlarray);

    //Remove the sequence
    $db->DropSequence( cms_db_prefix().'module_catlist_products_seq' );

    //Remove the permission
    $this->RemovePermission('Catlist Admin');
}

Similar to the Install steps, we want to provide a message to the administrator letting them know that the un-installation succeeded. Also, we want to provide an "are you sure?" message that the administrator will be prompted with before un-installing the module (this is especially important since there may be product data in our database table that would be deleted). As usual, add these functions to the Catlist.module.php file (within the curly braces of the Catlist class):

function UninstallPreMessage()
{
    return $this->Lang('uninstall_confirm');
}

function UninstallPostMessage()
{
    return $this->Lang('postuninstall');
}

And of course, add the english version of these messages to the "lang" file:

$lang['uninstall_confirm'] = 'All product data in the catalog will be deleted!'
                           . 'Are you sure you want to uninstall the Catlist module?';
$lang['postuninstall'] = 'Catlist successfully un-installed.';


Okay, we finally have enough code to test our new module out!

Displaying Products On The Front-End Site

As stated earlier, we are going to create a function in our Catlist.module.php file called "DoAction" -- this function is called when a {cms_module module='Catlist'} tag is encountered in a page, and it will respond by outputting html (in this case, to show a listing of all the products in our database). Add this inside the Catlist class in the Catlist.module.php file:

 function DoAction($action, $id, $params, $returnid=-1)
 {
 
 }

For the time being, we will ignore the function parameters, but they must be in the function declaration, otherwise an error will occur. When your module outputs different html under different circumstances (for example, on the normal page view we will output catalog products, but in the admin panel, we will output a form allowing data entry to be performed). Your function knows what to do based on the $action parameter. For our purposes, we only want to do something when our module tag appears in page content, and this action is called 'default'. So, let's flesh out the DoAction function a bit:

   function DoAction($action, $id, $params, $returnid=-1)
 {
   if ($action == 'default')
   {
     $db =& $this->GetDb();
     $sql = 'SELECT * FROM ' . cms_db_prefix().'module_catlist_products;';
     $dbresult =& $db->Execute($sql);
     
     $list = "<br /><br />\n";
     $list .= "<ul>\n";
     while ($dbresult && $row = $dbresult->FetchRow())
     {                    
         $list .= "<li><b>" . $row['name'] . '</b><br />';
         $list .= $row['description'] . '<br />';
         $list .= money_format('%n', $row['price']) . '<br />';
         $list .= "</li>\n";
     }
     $list .= "</ul>\n";
     $list .= "<br /><br />\n";
   }
   // assign to Smarty
   global $gCms;
   $this->smarty->assign('list', $list);
   /**
    *Insert: {cms_module module='Catlist'}{$list}
    *in your page template
    *But there has to be a way for this to work without the {$list} tag...
    **/
   return;
 }

This code queries the database table we set up in our Install function to retrieve all products. Then it loops through the query results, outputting html along the way (via the echo function). To test this out, you will need to add a sample product or two to the database. Since we don't have an admin panel set up yet to do this, you should insert a couple of records directly into the database (using phpMyAdmin, or some other such tool). Then, go to the site admin area, go to the Modules page (under the Extensions menu), find the Catlist module in the list, and click "Install". Then, add a new page (or modify an existing page), and add our module tag -- {cms_module module='Catlist'}{$list} -- to the content area. Save and view that page on your site (or hit the refresh button if you already had it up in the browser). Tada! You should see a list of the products you entered into the database.

If you get an error on the money_format function

The Windows version of PHP does not include the money_format function so the above will not run under something like Xammp on XP. The following is a replacement from the comments of the php manual- stick it in the same file we're editing, Catlist.module.php, but *outside* of the class we're working on throughout the tutorial:

 function money_format($format, $number)
 {
   $regex  = '/%((?:[\^!\-]|\+|\(|\=.)*)([0-9]+)?'.
             '(?:#([0-9]+))?(?:\.([0-9]+))?([in%])/';
   if (setlocale(LC_MONETARY, 0) == 'C') {
       setlocale(LC_MONETARY, );
   }
   $locale = localeconv();
   preg_match_all($regex, $format, $matches, PREG_SET_ORDER);
   foreach ($matches as $fmatch) {
       $value = floatval($number);
       $flags = array(
           'fillchar'  => preg_match('/\=(.)/', $fmatch[1], $match) ?
                          $match[1] : ' ',
           'nogroup'   => preg_match('/\^/', $fmatch[1]) > 0,
           'usesignal' => preg_match('/\+|\(/', $fmatch[1], $match) ?
                          $match[0] : '+',
           'nosimbol'  => preg_match('/\!/', $fmatch[1]) > 0,
           'isleft'    => preg_match('/\-/', $fmatch[1]) > 0
       );
       $width      = trim($fmatch[2]) ? (int)$fmatch[2] : 0;
       $left       = trim($fmatch[3]) ? (int)$fmatch[3] : 0;
       $right      = trim($fmatch[4]) ? (int)$fmatch[4] : $locale['int_frac_digits'];
       $conversion = $fmatch[5];
       $positive = true;
       if ($value < 0) {
           $positive = false;
           $value  *= -1;
       }
       $letter = $positive ? 'p' : 'n';
       $prefix = $suffix = $cprefix = $csuffix = $signal = ;
       $signal = $positive ? $locale['positive_sign'] : $locale['negative_sign'];
       switch (true) {
           case $locale["{$letter}_sign_posn"] == 1 && $flags['usesignal'] == '+':
               $prefix = $signal;
               break;
           case $locale["{$letter}_sign_posn"] == 2 && $flags['usesignal'] == '+':
               $suffix = $signal;
               break;
           case $locale["{$letter}_sign_posn"] == 3 && $flags['usesignal'] == '+':
               $cprefix = $signal;
               break;
           case $locale["{$letter}_sign_posn"] == 4 && $flags['usesignal'] == '+':
               $csuffix = $signal;
               break;
           case $flags['usesignal'] == '(':
           case $locale["{$letter}_sign_posn"] == 0:
               $prefix = '(';
               $suffix = ')';
               break;
       }
       if (!$flags['nosimbol']) {
           $currency = $cprefix .
                       ($conversion == 'i' ? $locale['int_curr_symbol'] :   $locale['currency_symbol']) .
                       $csuffix;
       } else {
           $currency = ;
       }
       $space  = $locale["{$letter}_sep_by_space"] ? ' ' : ;
       $value = number_format($value, $right, $locale['mon_decimal_point'],
                $flags['nogroup'] ?  : $locale['mon_thousands_sep']);
       $value = @explode($locale['mon_decimal_point'], $value);
       $n = strlen($prefix) + strlen($currency) + strlen($value[0]);
       if ($left > 0 && $left > $n) {
           $value[0] = str_repeat($flags['fillchar'], $left - $n) . $value[0];
       }
       $value = implode($locale['mon_decimal_point'], $value);
       if ($locale["{$letter}_cs_precedes"]) {
           $value = $prefix . $currency . $space . $value . $suffix;
       } else {
           $value = $prefix . $value . $space . $currency . $suffix;
       }
       if ($width > 0) {
           $value = str_pad($value, $width, $flags['fillchar'], $flags['isleft'] ?
                    STR_PAD_RIGHT : STR_PAD_LEFT);
       }
       $format = str_replace($fmatch[0], $value, $format);
   }
   return $format;
 }


Separate displayed html out into smarty dedicated templates

(Note : feel free to correct this paragraph : English is not my mother language)

Now we are going to see how to use the powerful template engine : Smarty. The aim of this part is to separate the core (how it works) of our module from its output display (how it looks).

First, we have to create a new directory called "templates". Inside this new directory, we will put all the dedicated templates that will be needed by our module. (ie. all the specific templates that we are going to build for the module). Let's create a new file called "display.tpl" In this new file, put the following code :

 <br />
<br />
<ul>
  {section name=element loop=$list}
    <li><b>{$list[element].name}</b><br />{$list[element].description}<br />{$list[element].price} € <br /></li>
    {/section}
</ul>
<br />
<br />

The previous template uses the Smarty syntax. The line <li><b>{$list[element].name}</b><br>{$list[element].description}<br>{$list[element].price} € <br></li> display the name, the description and the price of the current element. We use {section name=element loop=$list} to apply the same mechanism to all the items form $list.

Now we have our template. We need then to give all the items to the template in order to display them. Therefore, we modify the previous method DoAction Like this :

 function DoAction($action, $id, $params, $returnid=-1)
 {
   if ($action == 'default')
   {
     $db =& $this->GetDb();
     $sql = 'SELECT * FROM ' . cms_db_prefix().'module_catlist_products;';
     $dbresult =& $db->Execute($sql);
     
     // Creating a new array for all products
     $list = array();
     //For each product of the database
     while ($dbresult && $row = $dbresult->FetchRow())
     {   
         // Get the number of current items in the list
         $i = count($list);
         // Set the different params
         $list[$i]['name'] = $row['name'];
         $list[$i]['description'] = $row['description'];
         $list[$i]['price'] = $row['price'];
     }
     // assign to Smarty
     $this->smarty->assign('list', $list);
     // Display the populated template
     echo $this->ProcessTemplate('display.tpl');
   }
   return;
 }

Modify your template : insert: {cms_module module='Catlist'} instead of {cms_module module='Catlist'}{$list}.

Writing an admin page

English is not my mother language, so please correct my mistakes in the following text.

Well, what is a module without an admin page? - Nothing easy to use, so I will try to explain how to write an admin page.

As it is already explained, the content of a module for the frontend is genereted by the "default"-action. It is not much different with the admin section: In the admin panel, the action "defaultadmin" is called. So, let's write some code to manage our product list.

First of all, I extend the code of the last chapter by these lines:

function DoAction($action, $id, $params, $returnid=-1)
{
  if ($action == 'default')
  {
    // see the code above
  }
  if ($action == 'defaultadmin')
  {
    // we'll put our admin panel here
  }
  return;
}

So, what will be the content of our admin panel? These are the post important functions:

  • Adding, modifying and deleting products from the list
  • Edit the template, with which the products will be displayed in the frontend.

To provide easy access to these features, we will split the page in two tabs: One for the product list, and the other for the template.

Tabs can be created inside a module function with this code:

// choose the tab to display. If no tab is set, select 'shows' as the default
// tab to display, because - in my opinion - this is the mostly needed tab.
if (!empty($params['active_tab']))
  $tab = $params['active_tab'];
else
  $tab = 'shows';

// and finally, display all those tabs. First, setup the tabs, and than include
// the function.{tab}.php file, in which the tab's code is stored to keep this
// file a bit tidier.
echo $this->StartTabHeaders();
echo $this->SetTabHeader('shows', 'Shows', 'shows' == $tab ? true : false);
echo $this->SetTabHeader('template', 'Template', 'template' == $tab ? true : false);
echo $this->EndTabHeaders();

// Display each tab's content
echo $this->StartTabContent();

echo $this->StartTab('shows');
include 'function.shows.php';
echo $this->EndTab();

echo $this->StartTab('template');
include 'function.template.php';
echo $this->EndTab();
echo $this->EndTabContent();

Okay, I have to explain a bit:

  • The $tab variable is to store the tab the user wants to see. It may be that - for example after submitting a template change -- the user does not want to see the default admin tab, but the "templates" tab. So we check whether the parameter "active_tab" is set, and store its content to $tab. For the tab headers, we check whether its tab name is equal to the active tab, if true, set the tab active.
  • To display tabs, we need to start their headers first. Then we need to set all tab's headers(The arguments of SetTabHeader are: Identifier, Display Text, active tab).
  • At last, we'll display the the tabs' content. To keep the code tidy, I put the code for the tabs in other files.

To get the admin page actually included in the admin menu, you must be sure to have the functions HasAdmin(), GetAdminSection(), VisibleToAdminUser() and GetFriendlyName() functions written in your .module.php. GetFriendlyName() was defined earlier, but the rest weren't. Here's a brief description for them:

function HasAdmin()
{
  // Return true or false depending on whether you actually
  // want to add the admin page for your module to the admin menu.
  return true;
}
function GetAdminSection()
{
  // Tells, which tab we want to put the menuitem of our module.
  // Can be at least 'content', 'extensions' and 'usergroups' ,
  // maybe others too.
  return 'extensions';
}
function VisibleToAdminUser()
{
  // Depending on permissions, tell whether the menuitem 
  // can be shown.
  return true;
}


To be continued

More To Come

Todo:

  • Add an Admin Panel that allows for product data entry and editing.
  • Add a "product details" page that shows full details of a single product.
  • Add product images.
  • Integrate searching into the catalog
  • Separate displayed html out into smarty dedicated templates (almost done)
  • Separate different actions out into multiple files
  • Add ability to put products into categories (so a single page can show a single category's products, and different categories can be different sub-pages to provide easier and more sensible browsing via the built-in CMSMS menu system)
  • Add module parameters for display options (product category, pagination, number of columns, etc.)

User Handbook/Developers Guide/Module Tutorial

From CMSMS

Arvixe - A CMSMS Partner