45
Module Programming Part 1 In this section we will cover the basics of how a module functions. You should be familiar with the Basic Module Structure ∞ before proceeding to this stage. Calling a Module The Zikula Application Framework takes standard arguments in the calling URL. It is also possible to call Zikula modules within code, but this will be cover later. The arguments will be explained. Zikula runs exclusively from the index.php although we do have some wrapper scripts which will be covered later. index.php expects three arguments module, type, and func in the form index.php?module=<module>&type=<type>&func=<func> module The name of the module you want to call. If not specified will default to the module specified in the Zikula System Settings. In a standard install this is often the News module. So if you provided this link, index.php?type=<user>&func=<view> Zikula would look in the News module use the file pnuser.php and call the function news_user_view(). Zikula will know to get all files required from the module's own directory.

Module Programming Part 1

Embed Size (px)

Citation preview

Page 1: Module Programming Part 1

Module Programming Part 1 In this section we will cover the basics of how a module functions. You should be familiar with the Basic Module Structure∞ before proceeding to this stage.

Calling a Module

The Zikula Application Framework takes standard arguments in the calling URL. It is also possible to call Zikula modules within code, but this will be cover later. The arguments will be explained.

Zikula runs exclusively from the index.php although we do have some wrapper scripts which will be covered later.

index.php expects three arguments module, type, and func in the form

index.php?module=<module>&type=<type>&func=<func>

module

The name of the module you want to call. If not specified will default to the module specified in the Zikula System Settings. In a standard install this is often the News module. So if you provided this link,

index.php?type=<user>&func=<view>

Zikula would look in the News module use the file pnuser.php and call the function news_user_view().Zikula will know to get all files required from the module's own directory.

type

The type argument tells Zikula which file to find the function required. In effect you tell Zikula to include the file pn{$type}.php (in the module's own folder determined from the module argument above. If not specified, defaults to user - i.e., include pnuser.php in the module folder.

eg.

type=user means include the file pnuser.php (default if type not specified) type=admin means include the file pnadmin.php

Page 2: Module Programming Part 1

type=test means include the file pntest.php

func

The func parameter tells Zikula which function to call in the included file in the form {$module}_{$type}_{func}() The default value for func is 'main' if not specified.

e.g.

func=test calls the function modulename_type_test()

Examples Here are a number of examples to help you get a feel for how the calling process works. In your own modules you are expected to follow this scheme. It makes all modules standard, so others can understand your code and allows others to call functions within your module from other modules.

index.php?module=helloworld&type=test&func=addo look in modules/helloworldo include pntest.php o call the function helloworld_test_add()

index.php?module=helloworld&type=user&func=showo look in modules/helloworldo include pnuser.php - (default: type=user)o call the function helloworld_user_show()

index.php?module=helloworld&type=&func=viewo look in modules/helloworldo include pnuser.php o call the function helloworld_admin_view()

index.php?module=Feedback&type=admino Look in modules/Feedbacko Include pnadmin.phpo call the function feedback_admin_main() - (default: func=main)

index.php?module=Newso Look in modules/News foldero Include pnuser.php - (default: type=user)o call function news_user_main() - (default: func=main)

In summary, we can call specific functions from the URL by telling Zikula the module name, the file type, and the function name we wish to call. We will now explore functions a

Page 3: Module Programming Part 1

little more.

Writing the code for a module

Now it is time for some code. Before we can write our first function, we have some preliminary set up to do. The steps are

1. Get the new module folder and files (we will begin with a blank module)2. Rename the module folder and all the files in the module to your module name3. Add our code to the module4. Install and activate the module

Create the new module files

The latest distributions of Zikula do not contain a blank module. For this tutorial I have created a blank module∞ that will work. This is a bare bones module that contains stubs for the files that you need to create a module in zikula. Realize that this is a simple example to get you started and there may be better ways to create your modules in the future. Unzip the archive of the blank module and drop it into your modules directory just like any other module. Do not install it yet, we will do that after we have made some modifications.

Rename the module folder and the module name in all the files

Change the name of the folder to helloworld and use a text editor to open each file and change every instance of blank to helloworld. It would be useful to use the search and replace function to modify all these files, and you may find using an IDE such as eclipse∞a big help here. Every instance should be be changed, but if you miss a few, it should still work.

Second, do a search and replace for every instance of BLANK, replacing it with HELLOWORLD for the entire file structure inside the helloworld folder.

Finally, open up the version.php file located in modules->helloworld->pnlang->eng. Once the file is open, edit the definitions located here to the following code.

define('_HELLOWORLD_NAME', 'helloworld');define('_HELLOWORLD_DISPLAYNAME', 'helloworld');define('_HELLOWORLD_DESCRIPTION', 'A helloworld module for learning zikula!');

Page 4: Module Programming Part 1

Add the helloworld code to the module

Module functions

Module functions should return a mixed result, or boolean: they should never echo or print anything directly (to the screen). Information returned from the interface modules (pnuser.php and pnadmin.php) will very often be text. (Other information can be returned, but we will cover those cases later) Zikula will take the return value of the module function and process it, ultimately adding it to the appropriate place in the page layout and displaying it to the user. Later we will learn about pnRender and how to display content using page templates. Using pnRender and template is the preferred method for zikula, and very powerful. But for now we will keep it simple to demonstrate how to get your module working and displaying the text on the screen. Open up your pnhelloworld.php file and find the main function of the file. It should be named helloworld_user_main.

example:<?php

function helloworld_user_main(){    return 'hello world!';}

?>

Install and activate the module

Before calling the module function we have to install the module in Zikula. Installing the module informs zikula that it exists and creates the necessary data tables for the module to function. The steps are:

1. Login as administrator2. Goto Administration -> Modules3. Regenerate the modules list from the filesystem4. Select the new module helloworld5. Initialize and Activate

Page 5: Module Programming Part 1

For more information on installing modules see Installing Modules∞ in the user documentation. Once your module is activated you should be able to call this function with

http://mydomain.com/index.php?module=helloworld∞

Since we have not specified type or func, Zikula will assume type=user and func=main

Zikula will now display 'hello world!' in a nice little box in the center of the content window. Notice how it's rendered with all the rest of the page content. Congratulations, you have come a long way in understanding the module framework and how to get it up and running. In the next installment, we will work on remembering module data for the framework.

Module Variables

Your module is of course an interface for the data that you are storing. If you have not already, familiarize yourself with the Basic Module Structure∞ before proceeding. There are two classes of data that you might want to store. One I will call instance data (data that is involved with what your module does), and the other admin data, information that affects all instances of a module. For example, imagine that you have a movie rating database. The names of the movies, their year, the studio, major actors in the film, and the rating given would be instance data. The number of movies to display per page would probably be an admin variable that would control display. Zikula can store admin data for a module in it's registry. This page describes the very simple process for getting and storing admin data. We don't need to concern ourselves with any data access as we have three standardized calls which will get, set and delete values.

pnModSetVar()

pnModSetVar($module, $name, $value)

One would normally initialise module variables in the modules initialisation script pninit.php. This is done at module installation time only and registers the variables with the Zikula registry. Open up pninit.php in the helloworld folder, find the function helloworld_init and rewrite the function so it looks as shown below.

Page 6: Module Programming Part 1

// pninit.phpfunction helloworld_init(){    if (!DBUtil::createTable('helloworld')) {        return false;    }    pnModSetVar('helloworld', 'myname', 'Fred Bloggs');    pnModSetVar('helloworld', 'myage', 32);    //initialization successful return true    return true;}

pnModGetVar()

pnModGetVar($module, $name)

Back to our 'hello world' module lets add a function greetings to pnuser.php

// pnuser.phpfunction helloworld_user_greetings(){    // get the variable 'myname' which belongs to the module 'helloworld' from the registry    $myname = pnModGetVar('helloworld', 'myname');

    // get the variable 'myage' which belongs to the module 'helloworld' from the registry    $myage= pnModGetVar('helloworld', 'myage');

    $greeting = "Hello World.  My name is $myname and I am $myage";

    // function returns to Zikula Application Framework.    return $greeting;}

Do not rely on the return value to test for success of these calls! If the module variable does not exist, pnModGetVar() returns false. If your module var is likely to hold a boolean, you

might get into trouble. For example, you test to see if a variable is true, pnModGetVar cannot find that variable and returns false. This is not what you expected and could lead to poor results. It is better store such data as a string, 'on' for true and 'off' for false, to avoid

Page 7: Module Programming Part 1

problems.

pnModDelVar()

pnModDelVar($module, $name)

This function would normally be used during the uninstall of a module in pninit.php It removes the variables from the registry.

// pninit.php// function to uninstall modulefunction helloworld_remove(){    pnModDelVar('helloworld', 'myname');    pnModDelVar('helloworld', 'myage');

    // delete all module vars in one step    pnModDelVar('helloworld');        return true;}

Display with pnRender

In the previous section we returned text directly to the zikula api. That is great for learning, but it completely defeats the purpose of using a database and a CMS. The goal is to separate your data from the html code that displays that data. When done well, this allows the changing of the appearance of a site, its theme, without affecting the data that is displayed. This magic is accomplished using templates and pnRender, which is a Zikula extension of the smarty template engine∞ that I will call smarty throughout this tutorial. In this section we will begin to explore how to use pnRender templates to display your content. If you have not already, familiarize yourself with the Basic Module Structure∞ before proceeding

This is an extremely important step forward with web applications. It is similar to the idea of database reports that display data from a table or query.

Templates are stored in the modules own pntemplates subdirectory. The naming scheme is {$module}_{$type}_{$function}.htm Templates can be named using extensions other than

Page 8: Module Programming Part 1

.htm--many people, for example, use .tpl. So function helloworld_user_greetings() would typically use the template helloworld_user_greetings.htm

A few words about smarty

Smarty is a very powerful processor that can greatly simplify the generation of html display code from a database. To use it in Zikula you follow four steps

1. Get an instance of pnRender from zikula. This is done with a simple call pnRender::getInstance(). You can specify the calling module, and whether to cache the result. By specifiying the calling module, you are telling Zikula what folder to look for templates. These will normally be stored with your module. Caching of the result allows zikula to save the page in the cache and for pages that do not change much from call to call, this can really increase performance on high traffic sites. (There is another parameter to getInstance that is really useful and we will talk about it in just a bit.)

2. Get any necessary data that is required for the template from the database (or elsewhere).

3. Assign this data to template variables using calls to assign. Assign takes two parameters, the template variable to assign the data to, and the variable you are adding. A typical call would be $pnRender->assign('myname', $myname);. It is often a good idea to use the same variable name for the template variable as well as the function variable.

4. Finally, generate the finished html code by calling fetch. Calling fetch tells smarty to generate the specified template using the variables that we previously assigned to this pnRender object. The engine will return the generated html code.

The best way to learn templates is to work with them so let's explore an example where we actually use pnRender to return code to the browser (building upon our previous "hello world" pnuser.php code snippet):

<?php

// pnuser.phpfunction helloworld_user_greetings(){    // Get the variable 'myname' which belongs to the module 'helloworld' from the registry.    $myname = pnModGetVar('helloworld', 'myname');     // Get the variable 'myage' which belongs to the module 'helloworld' from the registry.    $myage= pnModGetVar('helloworld', 'myage'); 

Page 9: Module Programming Part 1

    // Create a new pnRender object.    $pnRender = pnRender::getInstance('helloworld', false);;

    // Assign our variables to the template - this create a variable called 'myname'     // and assigns the value in $myname so it can be used in our template.    // Please note that keeping variables sensibly named and consistent is very helpful.    // You don't HAVE to do this, but you're asking for confusion if you don't    $pnRender->assign('myname', $myname);        // this create a variable called 'myage' and assigns the value in $myageso     //it can be used in our template.    $pnRender->assign('myage', $myage);            // the fetch call, tells smarty to process the template    //and return the results    return $pnRender->fetch('helloworld_user_greetings.htm');}?>

The only other part to fill in is the template. Templates are a mixture of html code and variables. You can layout the html code any way you like. Any legal html tag can be used

in a template. Honestly, you can put anytying you like in a template, but if you are displaying html code, the finished product should meet the latest html specification. Lets

start with a simple tempate --pntemplates/helloworld_user_greetings.htm:

<div> Hello World! My name is <!--[ $myname ]--> and I am <!--[ $myage ]-->. </div>

Note how our two variables are identified, pnRender (aka smarty) tags in Zikula are started by <!--[ and ended by ]-->

In the example above <!--[ $myname ]--> simply means insert the variable called $myname - this was the variable we assigned in our code before 'fetching' the template. Note that its very important to not have a space between the <!-- and [. Once you have this set up, call

the function by going to this URLhttp://yourzikulainstall/index.php?module=helloworld&func=greetings∞

Remember to uninstall and then install the module before doing this. (We have to add the two module variables to the module and this is only done in the initialize function. This

function is only called during the install of the module.) If you get an error saying that the

Page 10: Module Programming Part 1

page cannot be found, read the message carefully and it will tell you what the problem is. A common cause of failure is that the template name in the fetch call will not match the

template name in the pntemplates directory of the helloworld module.

Just for fun, lets introduce you to a little Zikula magic. Since functions will often make use of module variables in their templates, the Zikula developers have added a handy short cut.

Here is a reworked code snippet<?php

// pnuser.phpfunction helloworld_user_greetings(){    // create a new pnRender object.    $pnRender = pnRender::getInstance('helloworld', false, '', true);

    // fetch the template named, process it and return the result.    return $pnRender->fetch('helloworld_user_greetings.htm');}?>

with this template <div> Hello World! My name is <!--[ $pncore.helloworld.myname ]--> and I am <!--[ $pncore.HelloWorld.myage ]-->. </div>

By calling getInstance, with the fourth parameter set to true, it tells Zikula to add all core data (your module variables) to the template. You can then add them to your template code. -- all variables are assigned to $pncore.<modulename>. Zikula is full of nice little functions

like this that save the module developer hundreds of lines of code.

Plugins

Plugins are functions we can call while inside the display template. Smarty, which pnRender is based on, has several built in 'plugins' or functions. We can also use Zikula system wide plugins, or define our own for our specific module. Plugins can be passed arguements like a normal function.

Zikula system wide plugins can be found in [root]/system/pnRender/plugins and module specific plugins in our module folder in pntemplates/plugins

Page 11: Module Programming Part 1

Plugin files are named function.pluginname.php and contain *one* function inside calledsmarty_function_{$pluginname}

A plugin is called within a template like this:<!--[ myplugin ]-->

The example above will include the file in pntemplates/plugins/function.myplugin.php and call the function

smarty_function_myplugin()

Lets walk through a simple example that checks the age of the person in $myage, and then returns alternative text depending upon that age. First, add this line to the bottom of your

template helloworld_user_greetings.htm that you saved in the helloworld/pntemplates directory.

<!--[ agecheck myage=$myage ]-->

The example above will include the file in pntemplates/plugins/function.agecheck.php and call the function

smarty_function_agecheck() and parse the variable 'myage' to the function.

Variables are passed in an associative array of (name => value). Write the following code in a word processor and then save it as the file function.agecheck.php in the

helloworld/pntemplates/plugins/<?php//example plugin agecheck

function smarty_function_agecheck($params, &$smarty){    // pull the agecheck variable out of the $params    $agecheck = (int)$params['myage'];    if($agecheck < 18) {        $result = "You are under 18 years old";    }else{        $result = "You are 18 years or over";    }

    return $result;}?>

The $smarty object will be the same pnRender instance that called the plugin. This means the plugin can also modify the pnRender object. Our function could do something like

$smarty->assign('foo', 'bar');

Page 12: Module Programming Part 1

Which would mean the variable $foo would now be available to the template. You may be wondering why would you use a plugin, when you can just add the variable to the

template? Plugins are very valuable for two reasons. First, they can be reused in many templates by adding a single line that calls them. Second, they can use php code to process a template while it is being rendered. Some very powerful things can be done with them.

Modifiers

A modifier is a different kind of plugin. They are found system wide in [root]/system/pnRender/plugins or in a module's own pntemplates/plugins fodler.

Files named modifier.{$name}.php and contain one function called smarty_modifier_{$name}will be passed at least one string. And they will then process that string. One system wide modifier, pnvarperpdisplay, will clean a template variable for diplay as html code.

<!--[ $myname|pnvarprepdisplay ]--><?phpfunction smarty_modifier_pnvarprepdisplay($string){    return pnVarPrepDisplay($string)}?>

So lets add a modifier that bolds the age if it is over 30. Clearly over the hill if you ask me. Place the following code in a text file and then save it as modifier.overhill.php in the

helloworld/plugins directory.<?phpfunction smarty_modifier_overhill($string){    $age = (int)$string;    if($age > 30){        return "<b>" . $age . "</b>";    }    return $age;}?>

Now add change the code slightly in your template; change <!--[ $myage ]-->

to<!--[ $myage|overhill ]-->

Page 13: Module Programming Part 1

When you reload the page, the age of the variable should be bolded of it is over 30.

Output Filters

Module Programming Part 4 - Searching

To be able to find your items via the normal search function provided by Zikula, you need to implement Zikula's Search API interface - that is, you must create an API file named "pnsearchapi.php" in your module's root directory and implement a couple of specific functions in it.

But let's first take a look at how searching actually work. Here is what happens when a search form is created:1. The Search module scans all modules for an implementation of the Search interface.2. Each found module's search "plugin" is then asked to generate the HTML needed for any special search settings (like selecting a subset of the available topics or restricting search to a certain forum).3. All the search HTML is concatenated and presented to the user as the complete search form.

Now the actual searching starts:1. The user enters a search query - some keywords like "Zikula administration" - and fills out other search elements.2. The Search module scans all modules for an implementation of the Search interface.3. Each search plugin is passed the search query and other data from the search form and is asked to make a search of it in all the module's items.4. All items found by the plugin must be inserted in the Search module's result database table.5. When all modules are finished searching then the search module presents all the found items from it's result table and lets the user browse through them.

So the most important thing for your search plugin to do is 1) To create a search form, 2) to find items based on the search query, and 3) insert information about the found items in the Search module's result table. This is done for the following reasons:1. All search results are presented in the same way independently of the module they originate from.2. Paging, browsing, and sorting can all be done through SQL. This performs a lot better than doing so in PHP (for "large" result sets - wherever that threshold is).3. The search plugins are only activated on the first page. Subsequent browsing through the results is done much more efficiently by browsing through the result table.

Page 14: Module Programming Part 1

Interacting with the search module

Your pnsearchapi.php file must include a "info" function that passes various required information about the module back to the Search module.

The returned info data is an array with the following keys:1. title - the name of the module (must match the name used in the search options template exactly).2. functions - an array of document types and their search function. The Search module will iterate over each document type in the array and call the corresponding search functions when doing the search.

The example here is from the FAQ module:

function faq_searchapi_info(){    return array('title' => 'FAQ',                  'functions' => array('FAQ' => 'search'));}

Creating the search form

You need to implement the "options" function that returns the input form's HTML code. Here's an example from the FAQ module:

function faq_searchapi_options($args){    if (SecurityUtil::checkPermission( 'FAQ::', '::', ACCESS_READ)) {        $pnRender = new pnRender('FAQ');        return $pnRender->fetch('faq_search_options.htm');    }

    return '';}

The corresponding pnRender template is:<div>

<input type="checkbox" id="active_faqs" name="active[FAQ]" value="1" checked="checked" />

<label for="active_faqs"><!--[pnml name="_FAQ_CATEGORIES]--></label>

Page 15: Module Programming Part 1

</div>

Performing the search

Now, when the user submits the search query, the Search module starts scanning for search plugins. For each found plugin it locates the "info" function, fetches the "functions" list, and iterates through each document type's search function. Typically there will only be one type with a search function named "search" - corresponding to (for instance) "FAQ_searchapi_search".

A typical search function performs these steps:1. Access check - does the current user have access to searching through this kind of documents?2. Loading database information.3. Create an SQL "select" statement based on the search query.4. Execute the "select" statement and iterate through the found items.5. Check each found item for item based access. If the user has access to it then it is inserted in the Search module's result table.

A very important step here is to "decorate" all data in the result table with the current user's session ID. This is done so multiple users can do searching simultaneously without seeing each others results.

We need to insert the following values in the result table:1. title - the title of the found item.2. text - any relevant text representing the item.3. extra - any data needed to create a link to the item (typically the item ID).4. created - date of creation of the item.5. module - exact name of the module that owns the item.6. session - the session ID of the current user.

Here is how it is done in the FAQ API:

function faq_searchapi_search($args){    pnModDBInfoLoad('Search');    $pntable = pnDBGetTables();    $faqtable = $pntable['faqanswer'];    $faqcolumn = $pntable['faqanswer_column'];    $searchTable = $pntable['search_result'];    $searchColumn = $pntable['search_result_column'];

    $where = search_construct_where($args,

Page 16: Module Programming Part 1

                                    array($faqcolumn['question'],                                           $faqcolumn['answer']),                                     null);

    $sessionId = session_id();

    $sql = "SELECT    $faqcolumn[question] as title,   $faqcolumn[answer] as text,   $faqcolumn[faqid] as id,   $faqcolumn[cr_date] as dateFROM $faqtableWHERE $where";

    $result = DBUtil::executeSQL($sql);    if (!$result) {        return LogUtil::registerError (_GETFAILED);    }

    $insertSql = "INSERT INTO $searchTable  ($searchColumn[title],   $searchColumn[text],   $searchColumn[extra],   $searchColumn[created],   $searchColumn[module],   $searchColumn[session])VALUES ";

    // Process the result set and insert into search result table    for (; !$result->EOF; $result->MoveNext()) {        $item = $result->GetRowAssoc(2);        if (SecurityUtil::checkPermission('FAQ::', "$item[id]::", ACCESS_READ)) {            $sql = $insertSql . '('                    . '\'' . DataUtil::formatForStore($item['title']) . '\', '                   . '\'' . DataUtil::formatForStore($item['text']) . '\', '                   . '\'' . DataUtil::formatForStore($item['id']) . '\', '                   . '\'' .

Page 17: Module Programming Part 1

DataUtil::formatForStore($item['date']) . '\', '                   . '\'' . 'FAQ' . '\', '                   . '\'' . DataUtil::formatForStore($sessionId) . '\')';            $insertResult = DBUtil::executeSQL($sql);            if (!$insertResult) {                return LogUtil::registerError (_GETFAILED);            }        }    }

    return true;}

Please refer to the code in your working copy of Zikula - the above code might easily be deprecated when you read this.

Adding URLs and last minute access checking

You may wonder why there was no "link-to-item" URL in the above code examples. This is because URLs must be generated via the pnModUrl() function - and we only want to call that function for the items actually displayed to the end user. So the item URL is added in a post processing step for each item displayed to the user.

This post processing is done with the "xxx_yyy_check" API function (where xxx=module name and yyy=search function name). This function is called by the search API and passed the database data for the found item:

// The ampersands are *very* important here! We need to modify the $args variablefunction faq_searchapi_search_check(&$args){    // Fetch database row and the "extra" value in this row    $datarow = &$args['datarow'];    $faqId = $datarow['extra'];        // Write URL into the data    $datarow['url'] = pnModUrl('FAQ', 'user', 'display', array('faqid' => $faqId));

    // User has access to this item - so return true    return true;}

Page 18: Module Programming Part 1

It is also possible to do late access checking at this time - but it should be avoided since the displayed count of found items will be too big if you filter out some items. If you need to

use late access checking then return true/false signalling access allowed/denied.

Speeding up searching

If it is possible to do searching without access control, or with an access control that can be expressed in SQL, then we can short-circuit the "select-from-database-into-PHP-and-insert-into-database-again" step.

Instead of going through PHP we just do a "INSERT INTO result-table SELECT ..." where the select statement is more or less the same as the normal search statement.

Passing of module specific variables

It is possible to pass variables specific for your module by adding inputs to your form like

<input name="modvar[yourmodulename][yourparametername]" value="yourparametervalue" type="hidden">

You will receive the variable in the args array passed to your yourmodule_pnsearchapi_searchfunction. You will thus be able to use the parameters

passed to modify your search implementation.

Implementation details

One problem is that the search module won't be informed when a session is deleted. This means the result table will grow and grow, keeping up with an ever increasing amount of unused results stored for unused sessions. To avoid this the system adds a timestamp to each found item. Items older than some hours are automatically deleted.

Writing Module Initialization Functions

Page 19: Module Programming Part 1

Introduction

When you write a new module, it is your hope that someone will find it useful, download it, and install it. In order to install it, Zikula requires that you write an initialization process. The goal of this initialization is to bring all of the module variables, database tables, and their contents up to some initial baseline state, so that the module can be used.

When you sit down to write or update your module, keep in mind what the initial state of the module must be for a fresh installation. What should the initial values for your module variables be? What tables need to exist? Should they have any contents pre-loaded? All of this information will go into the initialization functions you write.

The < moduleName > _init() Function

The <moduleName>_init() function (where "<moduleName>" is the prefix you are applying to all of your module functions) is the workhorse of your module's initialization process. This is the function that should control all aspects of brining your module into its baseline state. When an administrator clicks on "Initialize" in his list of modules, this is the function called1 by Zikula's process, to perform any module-specific actions.

The function does not accept any parameters. It is required to return a boolean value. You should return true if the initialization was successful, and false if it was unsuccessful for any reason.

What goes function's signature and the final return value of the function is really up to you. You should ensure that every thing that needs some sort of initial value or state gets it.

Listing 1. Example initialization function framework.

1. function mymodule_init()2. {3.     if (!DBUtil::createTable('my_main_table')) {4.         return false;5.     }6.  7.     if (!DBUtil::createTable('my_other_table')) {8.         return false;9.     }10.  

Page 20: Module Programming Part 1

11.     // populate default table contents12.     // (insert a bunch of SQL stuff executed by

DBUtil here)13.  14.     // populate default module variable values15.     pnModSetVar('mymodule', 'myModuleVar', 25);16.  17.     // Initialisation successful18.     return true;19. }

This is a fairly basic function, and you can make it as simple as you want (down to simply returning true), or as complex as you want.2 With Zikula version .8 it is now possible to

store structural data dictionary information in the array returned by your module's <myModule>_pntables() function. This, along with the DBUtil class, makes the process

of adding database tables and columns a bit easier.Users of Zikula .7x write a similar function, however the DBUtil class is not available to you. You have to construct the proper SQL statements, and execute them directly. For example:    $dbConn =& pnDBGetConn(true);    $pnTables =& pnDBGetTables();    $myMainTable = $pnTables['my_main_table'];    $myMainColumns = $pnTables['my_main_table_column'];

    $sql = "CREATE TABLE ".$myMainTable."...   // the rest of the SQL statement    $result = $dbConn->Execute($sql);    if (!$result === false) {        // ... do error handling stuff    }    // ... do more init stuff 

Interactive Initialization: The < moduleName > _init_interactiveinit() Function

If there is nothing to ask your user during the initialization process, and you (and they) are happy setting defaults for any module variables (or anything else), then all you really need to implement is the <moduleName>_init() function.

If, on the other hand, your initialization process needs direction from the user, or you would

Page 21: Module Programming Part 1

like to give them the opportunity to set values for variables other than the default, then you can implement the <moduleName>_init_interactiveinit() function as well. Please note that this function does not replace the <moduleName>_init() function, but augments it.

Because of the way the Modules system module determines whether an interactive initialization should be called or not, getting the flow between your interactive initialization function, your initialization function, and the Modules system module takes a little bit of alchemy. Once you see what is required, however, you'll see that it is quite easy.

The basic flow of events in an interactive initialization are as follows:1. The user clicks on "Initialize" in the module list of the Modules system module.2. The Modules system module checks to see if a function called <moduleName>_init_interactiveinit() exists--and if it has already been called or not.A. If the function does exist and has not yet been called, then it is called.A. The user proceeds through the one or more pages it presents.A. The interactive function(s) store the gathered responses somewhere that they can be found later (most likely one or more session variables).A. The interactive function redirects the user back to the Module system module's initialization function (where it starts again at #1 from this list).3. The Modules system module (having determined that either the interactive function does not exist, or that has already been called) calls the <moduleName>_init() function.A. This function retrieves the responses gathered from the interactive process. If none are found, then it uses default values.A. It performs the necessary initialization steps, and returns true or false.4. The Modules system module updates its tables appropriately, and then returns to the list of modules.

The Modules system module, fortunately, can "auto-magically" determine whether the interactive process has already been called or not. It does this by setting a session variable called "interactive_init", and checking its value3 when the initialization function is called.

The simplest interactive initialization is one that presents one page to the user, gathers all of the information from that page, stores the information in one or more session variables, and then calls the initialization function on the Modules system module back. An example might be:

Listing 2. Example interactive initialization function framework.

1. function mymodule_init_interactiveinit($args) {2.     if (!pnSecAuthAction(0, 'mymodule::', 'admin::',

ACCESS_ADMIN)) {3.         return _MODULENOAUTH;4.     }5.  

Page 22: Module Programming Part 1

6.     // Get the passed parameters (basically just $args['oldversion'])

7.     extract($args);8.  9.     // See if we are here from a call-back from a form

submission.  This is 10.     // for us, and has nothing to do with the

Modules system module11.     // knowing that the interactive process is

complete.12.     $interactiveComplete =

pnVarCleanFromInput('interactiveComplete');13.  14.     if (empty($interactiveComplete)) {15.         // Initialize the $initOpts variable with

defaults, which will keep 16.         // track of the selected options for us.17.         $initOpts ['anOption'] = "a default value";18.         $initOpts ['anotherOption'] = 3; // another

default value19.         $initOpts ['activate'] = false; // default

for whether or not to activate after init20.  21.         $pnRender =& new pnRender('mymodule');22.         $pnRender->caching = false;23.         $pnRender->assign('initOpts ', $initOpts );24.         return $pnRender-

>fetch('mymodule_init_interactiveinit.tpl');25.     }26.     else {27.         // Get any submitted form variable values

since we are here from a 28.         // call-back from a form submission29.         $initOpts ['anOption'] =

trim(pnVarCleanFromInput('anOption'));30.         $initOpts ['anotherOption'] =

(int)pnVarCleanFromInput('anotherOption');31.         $initOpts ['activate'] =

pnVarCleanFromInput('anotherOption');32.         $initOpts ['activate'] = (!

empty($initOpts['activate']) ? true : false);33.  34.         // Save the initialization options to a

session variable, so we can35.         // retrieve them later in mymodule_init()36.         pnSessionSetVar('mymodule_initOpts',

serialize($initOpts));

Page 23: Module Programming Part 1

37.  38.         // Call back the initialization function

from the Modules system module,39.         // which will automagically know that we

have completed the 40.         // interactive process. We need to set a

new authid, since the last41.         // one was used and discarded.  Aso, we set

the activate parameter42.         // to inform the Modules system module of

the users choice43.         return pnRedirect(pnModURL('Modules',

'admin', 'initialise', array(44.                 'authid' =>

pnSecGenAuthKey('Modules'),45.                 'activate' => $initOpts['activate']46.                 )));47.     }48. }

The above example is fairly basic. If you require a more complex interaction with the user, then you might want to split the process over several functions. There are several ways to

manage the user through a flow of several pages, and it is beyond the scope of this document to illustrate those methods. It is left as an exercise for the reader to determine his

best course of action.

The key concepts here are (a) how to record information for later use in the <moduleName>_init() function, and (b) how to call back to the main initialization

process.

The line:    pnSessionSetVar('mymodule_initOpts', serialize($initOpts));

is where the information gathered from the user is stored for later use. It should be coupled with:

    $initOpts = pnSessionGetVar('mymodule_initOpts');    $initOpts = deserialize($initOpts);

within the <moduleName>_init() function, to retrieve the information for use.

The line:    return pnRedirect(pnModURL('Modules', 'admin', 'initialise', array(            'authid' => pnSecGenAuthKey('Modules'),

Page 24: Module Programming Part 1

            'activate' => $initOpts['activate']            )));

is where the process is returned to the initialization process managed by the Modules system module. Note that this is a redirect to a UI function, not a pnModAPIFunc call. The UI function is the one that determines whether the interactive process should be called or not. The authid is reset for the "modules" module, because on the first time through, the authid that was available on the modules list was used up and discarded. The "activate"

parameter is optional (defaulting to false), but allows you to ask the user whether the module should be automatically activated at the end of a successful initialization or not.

Conclusion

Providing an initialization process for your modules is a relatively easy task, made more complex only by the complexity of what your module requires.

The <moduleName>_init() function is the workhorse of the process, and is where all of your custom module initialization code goes. It doe not accept any parameters, and returns a boolean value based on whether the initialization was successful or not.

The <moduleName>_init_interactiveinit() function allows you to gather information about the initialization process from the user. In the end, should redirect back to the initialization UI function from the Modules system module. Information gathered during the interactive process should be stored somewhere that the <moduleName>_init() function can later retrieve it and make use of it.

Writing Module Upgrade Functions

Introduction

Eventually you will add new features, update functionality, and/or fix defects in your module and release a new version. When you do this, and give your module a new version number, those who have installed an older module must use the Zikula upgrade function in the Modules module to get your new version working on their system.

Writing the function(s) necessary for performing upgrades is not difficult, but it does take some planning on your part.

Page 25: Module Programming Part 1

Planning for Upgrades

When you sit down to write the initial version of your module, the tasks required to later upgrade it to a newer version are probably the furthest thing from your mind. If you, however, plan for this eventuality, then writing the necessary upgrade functions later on will be a much easier task.The process of upgrading a module to a new version can involve a number of steps, including (but not limited to):

adding new module variables removing older module variables that are no longer used adding new database tables and columns to existing tables altering database tables and/or columns removing database tables and/or columns that are no longer used transforming data (either in a module variable or database tables) from an old

format to a new format

I'm sure you can think of other things that might need to be performed during an upgrade.

As modules become more mature, there may be several versions installed by various sites. Anyone wanting to upgrade to the newer version of your module will certainly appreciate that you have written an upgrade path for them. Ensuring that you can upgrade your module to its latest version from any older version means that you need to know what was added, what was removed, and what changed for each known version number that someone might have installed. Keeping track of this information is not difficult, but does require some work on your part.

Module Variables

When you first use a module variable, you should document the version number in which it first appeared in your module. Your initialization function probably gives the module variable a default variable, so this is one convenient place to record this information. You might have another more convenient place. In any case, be consistent, so that you do not have to search through your code every time looking for (and possibly missing) this information.

Likewise, the version that a module variable is removed from your module (or its format or contents are modified) should be documented somehow. The most obvious place is right in the upgrade function that removes (or modifies the contents of) the variable.

Database Tables and Columns

Page 26: Module Programming Part 1

When you first use a database table or column, you should document the version number in which it first appeared in your module. Your initialization function probably creates the table and/or column, so this is one convenient place to record this information. The moduleName_pntables() function in your pntables.php script is another likely place. You might have another more convenient place. In any case, be consistent, so that you do not have to search through your code every time looking for (and possibly missing) this information.

Likewise, the version that a table and/or column is altered or removed from your module should be documented somehow. The most obvious place is right in the upgrade function that alters or removes the construct.

Other Planning

Anything else that might be modified by an upgrade function in the future should have the version number where it first appeared recorded somewhere. Likewise, any modifications to those items.

The < moduleName > _upgrade() Function

The <moduleName>_upgrade() function (where "<moduleName>" is the prefix you are applying to all of your module functions) is the workhorse of your module's upgrade process. This is the function that should control all aspects of upgrading your module from one version to another. When an administrator clicks on "Upgrade" in his list of modules, this is the function called1 by Zikula's upgrade process to perform any module-specific actions.

The function accepts one parameter, $oldversion, which is the version string from the version of the module last installed (or upgraded to).

The function is required to return a boolean value. You should return true if the upgrade was successful, and false if the upgrade was unsuccessful for any reason.

What goes between the gathering of the parameter value and the final return value of the function is really up to you. You should ensure that every change between the version described in $oldversion and the new version are applied to the system.

Determining the Proper Upgrade Path

The $oldversion parameter passed in by Zikula is your primary source for determining the proper upgrade path. You must compare the value in $oldversion to the current version number, and decide what steps are necessary to get from "there" to "here."

Page 27: Module Programming Part 1

Comparing the old version number to a literal string with the "==" operator (or using the switch control structure) is one way of differentiating version values. Another possibility, if you have opted to use a PHP-style version-numbering plan for your module, is to use the PHP version_compare()∞ function. One advantage of using the standard version-numbering plan and the version_compare()∞ function is that you are then able to perform more complex boolean operations between version numbers without having to pull apart the version number elements on your own.

Comparing the version numbers is only the first step. After determining the version of the module being upgraded, you must then execute all of the steps necessary to actually get from "there" to "here." Many modules use a linear upgrade path as their strategy for performing upgrades. For most modules, this is probably quite adequate. The basic idea is to determine the currently installed version (from $oldversion), and then apply all of the upgrade steps, in order, from that version to the current version. To better organize the different sets of upgrade steps, many module authors put each set of changes in their own functions (e.g., mymodule_upgrade_100to110(), mymodule_upgrade_110to120(), and mymodule_upgrade_120to200()), and then call each of these functions in turn. An example of this (for version 2.0.0 of "mymodule") might be the following:

Listing 1. Example upgrade function framework.

1. function mymodule_upgrade($oldversion) {2.     if (!pnSecAuthAction(0, 'mymodule::', 'admin::',

ACCESS_ADMIN)) {3.         return _MODULENOAUTH;4.     }5.  6.     switch($oldversion) {7.         case "1.0.0" :8.             if (!mymodule_upgrade_100to_110()) {9.                 return false;10.             }11.             // fall through to the next case on

purpose12.         case "1.1.0" :13.             if (!mymodule_upgrade_110to_120()) {14.                 return false;15.             }16.             // fall through to the next case on

purpose17.         case "1.2.0" :18.             // nothing to do for 1.2.0 to 1.2.119.             // There were only changes to the PHP

code in version 1.2.120.             // (keep this note intact to remind

future developers, please)21.  

Page 28: Module Programming Part 1

22.             // fall through to the next case on purpose

23.         case "1.2.1" :24.             if (!mymodule_upgrade_121to_130()) {25.                 return false;26.             }27.             // fall through to the next case on

purpose28.         case "1.3.0" :29.             if (!mymodule_upgrade_130to_200()) {30.                 return false;31.             }32.             // DO NOT fall through -- last

upgradeable version33.             // Next case will be "2.0.0" when the

next version is released34.             break;35.         default:36.             // put the text in a pnLang variable!37.             pnSessionSetVar('errormsg', 'An unknown

version is installed!');38.             return false;39.     }40.     41.     return true;42. }

In the above implementation, the switch statement determines where to begin calling upgrade functions. Each _XXXtoXXX function, in turn, adds or removes module variables,

adds or removes (or alters) database tables, and adds or removes (or alters) database columns as necessary to move from one version to the next. Any other steps required to

move from one version to the next are also performed in each of these functions. It is beyond the scope of this document to describe these _XXXtoXXX functions any further, since each module's requirements will differ, and the upgrade steps that are needed between one

version and the next for any particular module will also differ. It is also probably obligatory to point out here that (a) there are many ways to structure your function2, and (b) for a small

set of upgrade steps, it is possible to perform them directly in the case clause, rather than calling a function.

With Zikula version .8 it is now possible to store structural data dictionary information in the array returned by your module's <myModule>_pntables() function. This, along with

the DBUtil class, makes the process of adding database tables and columns a bit easier. Altering and removing tables columns requires a bit of forethought3, but are also made

easier with the DBUtil class. Please see the documentation on this class for more information on the methods it makes available.

Page 29: Module Programming Part 1

Interactive Upgrades: The < moduleName > _init_interactiveupgrade() Function

If there is nothing to ask your user during the upgrade process, and you (and they) are happy setting defaults for any new module variables (or anything else), then all you really need to implement is the <moduleName>_upgrade() function. As was mentioned before, this is the workhorse of the upgrade, and all that is really required to accomplish a successful upgrade.

If, on the other hand, your upgrade process needs direction from the user, or you would like to give them the opportunity to set values for new variables other than the default, then you can implement the <moduleName>_init_interactiveupgrade() function as well. Please note that this function does not replace the <moduleName>_upgrade() function, but augments it.

Because of the way the Modules system module determines whether an interactive upgrade should be called or not, getting the flow between your interactive upgrade function, your upgrade function, and the Modules system module takes a little bit of alchemy. Once you see what is required, however, you'll see that it is quite easy.

The basic flow of events in an interactive upgrade are as follows:1. The user clicks on "Upgrade" in the module list of the Modules system module.2. The Modules system module checks to see if a function called <moduleName>_init_interactiveupgrade() exists--and if it has already been called or not.A. If the function does exist and has not yet been called, then it is called.A. The user proceeds through the one or more pages it presents.A. The interactive upgrade function(s) store the gathered responses somewhere that they can be found later (most likely one or more session variables).A. The interactive function redirects the user back to the Module system module's upgrade function (where it starts again at #1 from this list).3. The Modules system module (having determined that either the interactive function does not exist, or that has already been called) calls the <moduleName>_upgrade() function.A. This function retrieves the responses gathered from the interactive process. If none are found, then it uses default values.A. It performs the necessary upgrade steps, and returns true or false.4. The Modules system module updates its tables appropriately, and then returns to the list of modules.

The Modules system module, fortunately, can "auto-magically" determine whether the interactive process has already been called or not. It does this by setting a session variable called "interactive_upgrade", and checking its value4 when the upgrade function is

Page 30: Module Programming Part 1

called.

The simplest interactive upgrade is one that presents one page to the user, gathers all of the information from that page, stores the information in one or more session variables, and then calls the upgrade function on the Modules system module back. An example might be:

Listing 2. Example interactive upgrade function framework.

1. function mymodule_init_interactiveupgrade($args) {2.     if (!pnSecAuthAction(0, 'mymodule::', 'admin::',

ACCESS_ADMIN)) {3.         return _MODULENOAUTH;4.     }5.  6.     // Get the passed parameters (basically just

$args['oldversion'])7.     extract($args);8.  9.     // See if we are here from a call-back from a form

submission.  This is 10.     // for us, and has nothing to do with the

Modules system module11.     // knowing that the interactive process is

complete.12.     $interactiveComplete =

pnVarCleanFromInput('interactiveComplete');13.  14.     if (empty($interactiveComplete)) {15.         // Initialize the $upgradeOpts variable

with defaults, which will keep 16.         // track of the selected options for us.17.         $upgradeOpts['anOption'] = "a default

value";18.         $upgradeOpts['anotherOption'] = 3; //

another default value19.         $upgradeOpts['activate'] = false; //

default for whether or not to activate after upgrade20.  21.         $pnRender =& new pnRender('mymodule');22.         $pnRender->caching = false;23.         $pnRender->assign('upgradeOpts',

$upgradeOpts);24.         return $pnRender-

>fetch('mymodule_init_interactiveupgrade.tpl');25.     }26.     else {

Page 31: Module Programming Part 1

27.         // Get any submitted form variable values since we are here from a

28.         // call-back from a form submission29.         $upgradeOpts['anOption'] =

trim(pnVarCleanFromInput('anOption'));30.         $upgradeOpts['anotherOption'] =

(int)pnVarCleanFromInput('anotherOption');31.         $upgradeOpts['activate'] =

pnVarCleanFromInput('anotherOption');32.         $upgradeOpts['activate'] = (!

empty($upgradeOpts['activate']) ? true : false);33.  34.         // Save the upgrade options to a session

variable, so we can35.         // retrieve them later in

mymodule_upgrade()36.         pnSessionSetVar('mymodule_upgradeOpts',

serialize($upgradeOpts));37.  38.         // Call back the upgrade function from the

Modules system module,39.         // which will automagically know that we

have completed the 40.         // interactive process. We need to set a

new authid, since the last41.         // one was used and discarded.  Aso, we set

the activate parameter42.         // to inform the Modules system module of

the users choice43.         return pnRedirect(pnModURL('Modules',

'admin', 'upgrade', array(44.                 'authid' =>

pnSecGenAuthKey('Modules'),45.                 'activate' =>

$upgradeOpts['activate']46.                 )));47.     }48. }

The above example is fairly basic. If you require a more complex interaction with the user, then you might want to split the process over several functions. There are several ways to

manage the user through a flow of several pages, and it is beyond the scope of this document to illustrate those methods. It is left as an exercise for the reader to determine his

best course of action.

Page 32: Module Programming Part 1

The key concepts here are (a) how to record information for later use in the <moduleName>_upgrade() function, and (b) how to call back to the main upgrade

process.

The line:    pnSessionSetVar('mymodule_upgradeOpts', serialize($upgradeOpts));

is where the information gathered from the user is stored for later use. It should be coupled with:

    $upgradeOpts = pnSessionGetVar('mymodule_upgradeOpts');    $upgradeOpts = deserialize($upgradeOpts);

within the <moduleName>_upgrade() function, to retrieve the information for use.

The line:    return pnRedirect(pnModURL('Modules', 'admin', 'upgrade', array(            'authid' => pnSecGenAuthKey('Modules'),            'activate' => $upgradeOpts['activate']            )));

is where the process is returned to the upgrade process managed by the Modules system module. Note that this is a redirect to a UI function, not a pnModAPIFunc call. The UI

function is the one that determines whether the interactive process should be called or not. The authid is reset for the "modules" module, because on the first time through, the

authid that was available on the modules list was used up and discarded. The "activate" parameter is optional (defaulting to false), but allows you to ask the user whether the

module should be automatically activated at the end of a successful upgrade or not.

Conclusion

Providing an upgrade process for your modules is a relatively easy task, made more complex only by the complexity of the actual upgrade steps it requires, and by how much you remember about past versions.

Planning for upgrades from the very first version of your module is important. Documenting when upgradeable items were added to your module, and then later modified or removed from your module, will help you immensely when it is time to write a new chunk of upgrade code.

The <moduleName>_upgrade() function is the workhorse of the upgrade process, and is

Page 33: Module Programming Part 1

where all of your custom upgrade code goes. It only accepts one parameter ("$oldversion"), and returns a boolean value based on whether the upgrade was successful or not.

The <moduleName>_init_interactiveupgrade() function allows you to gather information about the upgrade process from the user. It will receive the "oldversion" value in a parameter within the $args array, and in the end, should redirect back to the upgrade UI function from the Modules system module. Information gathered during the interactive process should be stored somewhere that the <moduleName>_upgrade() function can later retrieve it and make use of it.