Geek Moot '09 -- Introduction to CMS Module Development - Presentation Transcript
Good Morning
An Introduction to
Module Development
An Introduction to
Module Development
Or: How To Create Modules Without Becoming a
Bitter, Angry Little Man
What is a module?
• adds functions or capabilities to CMSMS
• allows more complexity than a plug-in or
user-defined tag (UDT)
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
What else does the
module API offer?
• database abstraction (ADODB)
• templating system (Smarty)
• user-input sanitizing
• persistent preferences
• access to CMS configurations
• dependencies management
Module API, cont.
• admin-side permission system
• admin-side tools for tabbed interfaces
• form utilities
• ... and more ...
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
Ready? Let’s go!
• “Hello World” module
• create the directory:
$CMSROOT/modules/HelloWorld
• create the initial module file:
HelloWorld.module.php
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!';
}
}
?>
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
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!”
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
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');
}
}
?>
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
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
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!”
Solutions
• output should use a template to allow
different formatting
• module should use the DoAction method to
provide alternate functionality
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/
Changes to HelloWorld.module.php
function DoAction($action, $id, $params, $returnid=-1)
{
$this->smarty->assign_by_ref('mod',$this);
$this->ProcessTemplate('hello.tpl');
}
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
Multiple actions
• build out the DoAction method
• initially, we’ll implement it in-line in the
module
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;
}
}
}
DoAction
• special action: default
• we’ll see another special action later
• other parameters will be explained later
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');
}
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.';
?>
Take a breath
• what we’ve covered so far:
• basic module file
• localization
• templates
• multiple actions
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.”
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
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) );
}
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.';
?>
Working backwards
• separate files
• method.install.php
• method.upgrade.php
• method.uninstall.php
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
method.install.php
<?php
// security measure
if (!isset($gCms)) exit;
$this->CreatePreference('use_random_phrase','y');
// entry in admin log
$this->Audit( 0,$this->GetName(),
$this->Lang('installed', $this->GetVersion()) );
?>
method.upgrade.php
<?php
// safety measure
if (!isset($gCms)) exit;
switch($oldversion)
{
case "0.0.0.1":
$this->CreatePreference('use_random_phrase','y');
}
$this->Audit( 0, $this->GetName(),
$this->Lang('upgraded', $newversion));
?>
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
action.action_name.php
• has $gCms defined
• has $id, $params, and $returnid defined
(more on these later)
• has $smarty defined
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.
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
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!”
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
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') ));
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
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');
}
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
Solution
• need to create an admin area for the module
• and need to create an input form
Creating an admin
• HasAdmin() method returns true
• our other magic action: defaultadmin
• separated file: action.defaultadmin.php
Additions to HelloWorld.module.php
function HasAdmin()
{
return true;
}
function VisibleToAdminUser()
{
return $this->CheckPermission('Manage All Content');
}
action.defaultadmin.php
<?php
if (!isset($gCms)) exit;
if (! $this->CheckPermission('Manage All Content')) exit;
if (isset($params['random']) && !empty($params['random']))
{
$this->SetPreference('use_random_phrase',$params['random']);
$smarty->assign('message',$this->Lang('preference_set'));
}
$smarty->assign('start_form',
$this->CreateFormStart($id, 'defaultadmin', $returnid));
$smarty->assign('input_pref',$this->CreateInputHidden($id,'random','n').
$this->CreateInputCheckbox($id, 'random', 'y',
$this->GetPreference('use_random_phrase','y')).
$this->Lang('title_use_random_phrase'));
$smarty->assign('submit', $this->CreateInputSubmit($id, 'submit',
lang('submit')));
echo $this->ProcessTemplate('adminpanel.tpl');
?>
What we’ve covered
API Covered API Not Covered
10%
90%
Resources
• the Wiki
http://wiki.cmsmadesimple.org/index.php/Developers
• Skeleton module
http://dev.cmsmadesimple.org/projects/skeleton
• IRC
freenode.net #cms
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
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!
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
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.
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!
Closing
Closing
• Thanks for your attention
Closing
• Thanks for your attention
• Goodbye cruel world...
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()
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
?>
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';
User Warning Module source, cont.
function MinimumCMSVersion()
{
return "1.6";
}
function GetDependencies()
{
return array('CMSMailer'=>'1.73');
}
function SetParameters()
{
$this->AddEventHandler( 'Core', 'DeleteUserPre', true );
}
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
Contact me
• sjg@cmsmodules.com
• drop by next time you’re in L.A.
0 comments
Post a comment