3. An Introduction to
Module Development
Or: How To Create Modules Without Becoming a
Bitter, Angry Little Man
4. What is a module?
• adds functions or capabilities to CMSMS
• allows more complexity than a plug-in or
user-defined tag (UDT)
5. Why a module instead
of a plug-in or UDT?
• multi-language
• install / uninstall / upgrade
• trigger / handle events
• provide infrastructure and functions for
other modules
6. What else does the
module API offer?
• database abstraction (ADODB)
• templating system (Smarty)
• user-input sanitizing
• persistent preferences
• access to CMS configurations
• dependencies management
7. Module API, cont.
• admin-side permission system
• admin-side tools for tabbed interfaces
• form utilities
• ... and more ...
9. Types of modules
• plug-ins (e.g., News, Search, FormBuilder)
• content-type modules (e.g., Cataloger)
• infrastructure module support (e.g.,
CMSMailer, nuSOAP)
• administrative (e.g., ModuleManager)
• event handlers (e.g., UserWarning*)
• filters (e.g., RegTM*)
* modules written for this presentation, source in appendix
10. Ready? Let’s go!
• “Hello World” module
• create the directory:
$CMSROOT/modules/HelloWorld
• create the initial module file:
HelloWorld.module.php
11. HelloWorld.module.php
<?php
class HelloWorld extends CMSModule
{
function GetName()
{
return 'HelloWorld';
}
function IsPluginModule()
{
return true;
}
function DoAction($action, $id, $params, $returnid=-1)
{
echo 'Hello World!';
}
}
?>
12.
13.
14.
15. Congratulations!
• you’re done!
• next steps:
• publish Hello World on the Developer’s
Forge
• check in to svn or git
• mention the new module in the Forum
• wait for fame and fortune to roll in
16. Uh-oh!
• our first Bitter Angry Little Man alert!
• 3 people filed bug reports / feature requests:
• “why English only?”
• “Capitalizing ‘World’ is grammatically
incorrect.”
• “I want to use this module in German!”
17. Solving problems
• reject issue about `World’ capitalization
(“works for me!”)
• solve other problem by using language files:
• create directory HelloWorld/lang
• create initial language file: en_US.php
19. HelloWorld.module.php
<?php
class HelloWorld extends CMSModule
{
function GetName()
{
return 'HelloWorld';
}
function IsPluginModule()
{
return true;
}
function DoAction($action, $id, $params, $returnid=-1)
{
echo $this->Lang('hello_world_string');
}
}
?>
20. Translation
• by adding module to Translation Center, it
can be translated into any language
• add by setting up externals in svn
• instructions at
http://forum.cmsmadesimple.org/index.php/topic,2639.0.html
• results in lang/ext directory containing
translation files
22. Success!
• localized version of the module is available
• checked into the repository and published
on the Forge
• downloaded all over the world
• surely, fame and fortune must follow
23. Uh-Oh!
• another Bitter Angry Little Man alert!
• more bug reports / feature requests:
• “I want the message in <h2> tags”
• “text should be in <p> tags!”
• “I also want ‘goodbye world’ as an
option!”
24. Solutions
• output should use a template to allow
different formatting
• module should use the DoAction method to
provide alternate functionality
25. Templates
• create
CMSROOT/modules/HelloWorld/templates
directory
• create your template: hello.tpl
• templates use the Smarty markup language
• you will learn to read and love the Smarty
manual at http://www.smarty.net/manual/en/
27. Changes to HelloWorld.module.php
function DoAction($action, $id, $params, $returnid=-1)
{
$this->smarty->assign_by_ref('mod',$this);
$this->ProcessTemplate('hello.tpl');
}
28. Where we’re at
• output is now templated. Site developer
could edit the hello.tpl to make it output the
message in any way they wanted
• we still have only one action - the module
will only display the “hello world” string
29. Multiple actions
• build out the DoAction method
• initially, we’ll implement it in-line in the
module
30. DoAction in HelloWorld.module.php
function DoAction($action, $id, $params, $returnid=-1)
{
switch ($action)
{
case 'default':
case 'hello':
{
$this->_do_hello($id, $params, $returnid);
break;
}
case 'goodbye':
{
$this->_do_goodbye($id, $params, $returnid);
break;
}
}
}
31. DoAction
• special action: default
• we’ll see another special action later
• other parameters will be explained later
32. Adds to HelloWorld.module.php
function _do_hello($id, $params, $returnid)
{
$this->smarty->assign_by_ref('mod',$this);
echo $this->ProcessTemplate('hello.tpl');
}
function _do_goodbye($id, $params, $returnid)
{
$this->smarty->assign_by_ref('mod',$this);
echo $this->ProcessTemplate('goodbye.tpl');
}
36. Update lang file
<?php
/* CMSROOT/modules/HelloWorld/lang/en_US.php */
$lang['hello_world_string'] = 'Hello World!';
$lang['goodbye_world_string'] = 'Goodbye you cruel old planet.';
?>
37.
38. Take a breath
• what we’ve covered so far:
• basic module file
• localization
• templates
• multiple actions
39. Oh Noes!
• another Bitter Angry Little Man alert!
• 12 people filed bug reports:
• “I have one installation that accepts the
‘hello’ and ‘goodbye’ actions, and another
that’s the older version, and I’m confused.”
• “I have different versions on my sites and
can’t tell them apart easily.”
40. Solutions
• we should have the module report its
version
• this opens up a can of worms: module
installation, upgrades, uninstallation
• which opens up another can of worms:
separating files
42. Adds to module
function GetVersion()
{
return '1.0';
}
function Install()
{
$this->Audit( 0,$this->GetName(),$this->Lang('installed',
$this->GetVersion()) );
}
function Uninstall()
{
$this->Audit( 0,$this->GetName(),$this->Lang('uninstalled') );
}
function Upgrade($oldversion, $newversion)
{
$this->Audit( 0,$this->GetName(),$this->Lang('upgraded',
$newversion) );
}
43. update lang file
<?php
/* CMSROOT/modules/HelloWorld/lang/en_US.php */
$lang['hello_world_string'] = 'Hello World!';
$lang['goodbye_world_string'] = 'Goodbye you cruel old planet.';
$lang['installed'] = 'Hello World version %s installed.';
$lang['uninstalled'] = 'Hello World has been uninstalled.';
$lang['upgraded'] = 'Hello World upgraded to version %s.';
?>
45. Separate file features
• global handle to CMS is defined: $gCms
• security tip: test for the global, so people
can’t call the file directly and cause trouble
• other variables pre-defined based on
method that’s been split out
50. Separate actions
• DoAction can also be split into multiple files
• this keeps memory footprint smaller, code
organization more logical
• each file is called action.action_name.php
• these files also have special pre-set variables
55. Wha??
• where’s our “Hello World?”
• action “hello” is not the same as action
“default”
• we can either implement action “hello” by
creating action.hello.php, or change our
model such that this is the default.
56. Another deep breath
• what we’ve accomplished:
• keeping track of module versions
• providing a clean approach to module
upgrades and uninstalls
• breaking module into separate files for
better memory management
• set preferences, logged to the admin log
57. Uh-Oh!
• Bitter Angry Little Man alert!
• 16 people filed feature requests:
• “I should be able to load in multiple
messages, not just ‘hello’ and ‘goodbye’!”
• “Yeah, and they should have the option of
displaying randomly according to that new
preference you created!”
58. The database
• handle to database available via GetDb()
method
• supports ADODB functionality
• hides a lot of the complexity for table
creation. see
http://phplens.com/lens/adodb/docs-datadict.htm
59. Adds to method.install.php
// get database handle
$db = &$this->GetDb();
// mysql-specific, but ignored by other database
$taboptarray = array( 'mysql' => 'TYPE=MyISAM' );
// database-independent table creation
$dict = NewDataDictionary( $db );
$flds = "phrase_id I KEY AUTO,
phrase C(255)";
$sqlarray = $dict->CreateTableSQL( cms_db_prefix().
'module_hello', $flds, $taboptarray);
$dict->ExecuteSQLArray($sqlarray);
$db->Execute('insert into '.cms_db_prefix().
'module_hello (phrase) values (?)',
array($this->Lang('hello_world_string') ));
60. Don’t forget!
• implement similar code in the
method.upgrade.php
• bump the module’s version number to 1.1
• and implement code that uses the phrases
from the database
61. Revised function
function _do_hello($id, $params, $returnid)
{
$db = &$this->GetDb();
if ($this->GetPreference('use_random_phrase','y') == 'y')
{
$count = $db->GetOne('select count(phrase) from '.
cms_db_prefix().'module_hello');
$rand_line = rand(1,$count) - 1;
$res = $db->SelectLimit('select phrase from '.
cms_db_prefix().'module_hello',1,$rand_line);
if ($res && $row=$res->FetchRow())
{
$phrase = $row['phrase'];
}
}
else
{
$phrase = $db->GetOne('select phrase from '.cms_db_prefix().
'module_hello where phrase_id=1');
}
$this->smarty->assign('phrase',$phrase);
echo $this->ProcessTemplate('hello.tpl');
}
64. Fear & loathing
• Angry bitter little man alert!
• 23 users complained about not being able
to change the preference
• 47 users complained about the lack of a
form to add phrases
65. Solution
• need to create an admin area for the module
• and need to create an input form
66. Creating an admin
• HasAdmin() method returns true
• our other magic action: defaultadmin
• separated file: action.defaultadmin.php
77. Resources
• the Wiki
http://wiki.cmsmadesimple.org/index.php/Developers
• Skeleton module
http://dev.cmsmadesimple.org/projects/skeleton
• IRC
freenode.net #cms
78. Bitter, angry little man
• hitting a moving target; keeping up to date
• lots of complaints about the flavors of free
ice cream available
• give an inch, they’ll ask for a mile
• using module for what it was never meant to
do
79. Preparing for CMSMS 2.0
• beware of direct access to API class
variables! e.g., use $this->GetDb(), don’t use
$this->db
• no more PHP 4.x-isms
• callbacks are going away! use the event
system.
• lots more – ORM, etc, so beware!
80. Calguy’s cardinal rules
• Do not use members of the module object,
use accessors
• Split everything up into logical classes
• No separate entry points, use actions
• Use the permissions model liberally
• Use separate files for actions, tabs, install/
upgrade/uninstall actions
81. Calguy’s rules, cont.
• Cache data where practical data may be read
(if it's feasible/expected that the
more than one time in a request).
• Use DEFINES or class constants rather than
hardcoded strings
• Separate logic from display... use Smarty
• Provide the data to Smarty, let Smarty
display it.
82. Calguy’s rules, cont.
• Use ADODB’s parameter cleansing stuff,
don't build in all the params yourself.
• Keep your code clean.
• Get off my lawn, you kids!
88. RegTM Module source
<?php
# extremely simple filter module; adds registered trademark symbol to your company's name
class RegTM extends CMSModule
{
var $company_name = 'CMS Made Simple';
function GetName()
{
return 'RegTM';
}
function GetFriendlyName()
{
return 'RegTM';
}
function GetVersion()
{
return '0.1';
}
function GetAuthor()
{
return 'SjG';
}
function GetAuthorEmail()
89. RegTM Module source, cont.
function MinimumCMSVersion()
{
return "1.6";
}
function SetParameters()
{
$this->AddEventHandler( 'Core', 'ContentPostRender', true );
}
function DoEvent( $originator, $eventname, &$params )
{
if ($originator == 'Core' && $eventname == 'ContentPostRender')
{
$params['content'] = str_replace($this->company_name,
$this->company_name.'<sup>®</sup>',
$params['content']);
}
}
}
// end
?>
90. User Warning Module source
<?php
# extremely simple event-handler module; sends email to admins giving them warning when a user is deleted.
class UserWarning extends CMSModule
{
function GetName()
{
return 'UserWarning';
}
function GetFriendlyName()
{
return 'UserWarning';
}
function GetVersion()
{
return '0.1';
}
function GetAuthor()
{
return 'SjG';
}
function GetAuthorEmail()
{
return 'sjg@cmsmodules.com';
91. User Warning Module source, cont.
function MinimumCMSVersion()
{
return "1.6";
}
function GetDependencies()
{
return array('CMSMailer'=>'1.73');
}
function SetParameters()
{
$this->AddEventHandler( 'Core', 'DeleteUserPre', true );
}
92. User Warning Module source, cont.
function DoEvent( $originator, $eventname, &$params )
{
if ($originator == 'Core' && $eventname == 'DeleteUserPre')
{
$db = $this->GetDB();
$user = $params['user'];
$result = $db->Execute('select * from '.cms_db_prefix().'users where user_id <>?',
array($user->id));
if ($result)
{
$mail =& $this->GetModuleInstance('CMSMailer');
if ($mail != FALSE)
{
$mail->reset();
$mail->SetSubject('Admin User Deleted');
$mail->SetBody('FYI: CMS admin user "'.$user->firstname.' '.$user->lastname.
'" ('.$user->username.') has been terminated.');
}
while ($row = $result->FetchRow())
{
$mail->AddAddress($row['email'],$row['first_name'].' '.$row['last_name']);
}
$sent = $mail->Send();
}
}
}
}
// end