Maintain 3 comes with a sophisticated module system, allowing developers to flexibly extend its functionality and admins to just load the modules they need for their modules without carrying around unnecessary overhead.
When you want to contribute new functionality to Maintain, you therefore almost always want to write a module instead of changing the core codebase of Maintain. This document shows you, how.
If you feel your idea has to be implemented in the core codebase rather than a module, please contact us
and let us know about your considerations.
Starting off
To find out how about the code contribution process, please read the Maintain Code Contribution Guide before you start writing code. That guide also tells you about how to get a copy of the Maintain code base from SVN.
To contribute modules, you will also need a copy of the Maintain module repository, which is separate from the core codebase. You can receive it through the following command, executed from somewhere outside your maintain working copy.
Guide: Installing and enabling a module
To make you comfortable about linking modules into your instance of Maintain, we will walk you through installing and setting up an example module called Message of the Day (motd).
Putting it into place: All modules you want to install have to be in separate directories underneath the $maintain/modules directory. What you want to do when making a new module is symlinking it into the modules directory of your running Maintain development instance. If you ever have to remove it from there, it will be quite easy.
Configuring the module: A lot of modules need some sort of configuration. Module config files are called modulename.cfg. Every module requiring such a file should ship with a modulename.cfg.dist file that contains example config values and ideally sufficient documentation on what config values are valid. Good distribution config files also already have meaningful default values in them, so that you can at least run Maintain without problems after you installed the module.
Config files are in PHP's .ini file format
.
So in our example, make your own config file by copying the distributed one:
and editing it as desired with your favorite UNIX text editor (or not at all, if you are fine with the default settings therein).
Afterwards, switch to the directory $maintain/bin and execute the following two commands:
Now your module should be up and running! In this example, go to the login page and note that there is a message being shown underneath the login form.
Now you know how to install a module.
Writing your own module: Case Study
The easiest way to start writing your own module is duplicating another one and replacing the interior parts of it.
How modules work:
- All Modules in Maintain inherit from the Module class, which resides in $maintain/modules/module.php. To know which functions you can (or even have to) override in your module, this is the place you have to look at.
- There are some, so-called core modules that inherit from Core_Module, which is just an empty, abstract subclass of Module. The difference between a normal module and a core module is that core modules provide functionality that is vital for Maintain to run. Therefore they can not easily be disabled or even uninstalled. You should only make a module "core" if there really is no way to run Maintain without it. Most additional functionality will therefore not be flagged core.
Let's take a look at the (simplified) source code of motd.php:
<?php
require_once(MODDIR.'/module.php'); // (1)
/**
* Displays a message of the day on Maintain's login page
*/
class Motd extends Module // (2)
{
function needsConfigFile() { // (3)
return true;
}
function _install() // (4)
{
return true;
}
function _uninstall()
{
return true;
}
function initialize() {} // (5)
function register_hooks() // (6)
{
Module::add_filter('login_motd', '_motd', null, NO_ACCESS);
}
function _motd($container) // (7)
{
$thetext = "Welcome to Maintain";
$container->add(html_br());
$container->add($thetext);
return $container;
}
}
I numbered the important parts of the file for you:
1:
On top of your file, you should / have to require_once()
the module.php file so that the Module class is known to the PHP interpreter. Make sure not to use [require()|http://php.net/require without _once, because otherwise you will get an error for redefining an existing class.
2:
As mentioned before, our module extends the Module class, as (almost) every other module.
3:
needsConfigFile() is a special function that is supposed to return true if (surprise!) your module needs a config file. If not, or if your config file is optional, you can either return false here or just not override this function, since false is the default.
4:
The functions _install() and _uninstall() are special functions that get called when installing/uninstalling your module. They can contain things like creating database tables, making files and folders or checking preconditions for your module to work (such as the presence of a specific command on the system Maintain is running on).
These functions have to return true if their install operations were successful, false otherwise. Make sure to return true here even if you don't have anything to do. They default to false which will cause there installation/uninstallation to fail.
5:
initialize() is a constructor. Well, not really. But it is being called by the constructor as the first function ever executed on a new object. Advantages of this method over overriding the actual constructor: You don't have to worry about calling the parent constructor (which is vital because it does important things).
6:
register_hooks() is a central function in the Maintain module system. Here your module has to register itself to so-called module hooks. If and only if it connected itself here to some hook, it will be called later, when module hooks are executed. Note that you can hook your module up to any named hooks, even if they don't exist yet. This allows you to introduce your own module hooks in your modules and to respond to them with another function of the same module. To learn more about the register_hooks syntax as well as how to introduce your own hooks, look at the Module System documentation.
7:
This is where the magic happens: Your own, private module function. Or one of them. Give them any name you want, and do whatever you need inside of them. Note that the second parameter used in the register_hooks call in step 6 is '_motd' which is the name of this very function. Whenever the login_motd hook is executed, it will instanciate your module and call the _motd() function for you.
Naming Conventions
These are easy: Modules have to be in a directory underneath the modules/ folder. If your module is named myModule, you have to name your folder myModule, your actual module file myModule.php and your config file myModule.cfg. That's easy, isn't it? Note that module names are case sensitive.
Actions and Filters
There are two types of module hooks: Actions and filters. There is good documentation on how they work on the Module System documentation wiki page. Read it. The sections about apply_filter and do_action in that document will shortly explain to you what actions and filters do, and how.
In our example up here, in step 7, the motd function is called by a _filter hook called 'login_motd'. It gets a (not necessarily empty) PhpHTMLLib
container
which it can modify arbitrarily. Afterwards, it returns the modifed container to the caller. It will then be taken and either passed on to the next callee (if there is one) or eventually it will just be displayed.
For more information on what action and filter hooks are currently available in Maintain, see the module hook list. This list will grow every time a new hook is added to the Maintain core.
Important Globals
There are two types of variables available to a method being called by the module system. One of them are, obviously, the parameters passed to the function itself. For filter hooks, this is mainly the object being "filtered" (often a PhpHTMLLib container or a string), but also other parameters are possible, depending on the hook (see the hook list).
The second part is global and superglobal variables. There are two most important global variables Maintain offers for every part of the application:
- $user is the user object of the currently logged in user. To find out what the object can do, inspect the file $maintain/class/dao/extended/user.php.
- $conf is the global config file array. It represents the settings in your maintain.cfg file with the format: $conf['section']['item'].
If your module has a config file, there is a similar array available for you to use. See the config files section below.
As for superglobal variables, all of PHP's superglobals are available. Noteworthy: $_REQUEST[] contains all POST and GET variables which are important for module webpages. See the section on modpages below.
Config files
As mentioned above, you can have a config file, optionally or mandatory, for your module. It has to be called modulename.cfg. The format is pretty easy: It's PHP's .ini file format
just like the main Maintain config file.
Module Security
Modules have an optional parameter to allow disabling of the module without running any of the module code. This parameter is a constant; if the user access level is lower than the constant, that module will not be executed in the maintain module engine. For example, you have a module that hooks into 'action_module':
The function doAdminOnlyAction($args) would only be executed if the user were an admin, whereas the function doUserAction($args) would only be run by someone who was a zone user, zone admin, or admin. Other security checks are the responsibility of the module writer.
Module Dependencies
(NOTE: This functionality was implemented POST-3.0.0, meaning this will only be available in the SVN snapshots until another Maintain release)
Maintain now supports module dependencies. Users may specify by name modules that they want their module to be dependent on, and Maintain will ensure that the required modules are installed and enabled before the users module may be installed and enabled. Also, once all the modules are up and running, Maintain ensures that modules that are required by other modules are not disabled/uninstalled out from under the dependent module.
Taking advantage of this feature requires that you only know the names of the modules that you want your module to be dependent on. Then, overload the _depends() function such that it returns the name/s of the modules your module requires.
Furthermore, you may return an array of strings if your module has multiple dependencies.
After you specify which module/s your module requires, you can rest easy because there is no way that your module will install without it's required modules being installed. Also, your module will not enable unless all its required modules are enabled.
Once your module is installed and enabled, Maintain will not allow the modules that your module requres to be disabled/uninstalled out from underneath your module, essentially providing a safer environment for making specialized modules that require the functionality of others, as well as many other helpful uses.
Documenting Modules
Please take the time to document your modules correctly. There are three useful things you should think about providing:
- a README file. Tell people what your module does (maybe even how), who you are, and how they can contact you. This makes them comfortable about using your module and chances are, if there is a tiny little bug buried somewhere in the module, somebody will help you fix it.
- Document your classes and functions. Maintain uses the phpDoc
standard throughout its code so that developers can immediately see what a method is supposed to do, which parameters it takes and what it will return. To make others understand your code, document as you go. If you don't document your code, people might not use your module, and chances are it won't be included in Maintain's module list because undocumented modules are hardly maintainable.
- Comment your distributed config file (modulename.cfg.dist). If you tell people what the allowed values are for every single config option, you make it as easy as the ABC for them to use your module. If they have to read through your whole module code first to guess what else they could put in as a config option, you make it unnecessarily hard for them to use your module.
As always: If you have questions about correctly documenting your source code, feel free to ask in the forums
or similar.
Submitting New Modules to the Maintain Project
As described in the Maintain Code Contribution Guide, for every module there should be a task on the Maintain bug tracking system
(JIRA). If there is none by now, read the contribution guide
and make one.
You want to put your module directory and all files therein into a .tar.gz file and attach it to the task in the bug tracking system:
$ cd $maintain-modules-workingcopy
$ tar czf mymodule.tar.gz mymodule/
Then go to the task in JIRA and click attach file in the menu on the left hand side.
The module will be inspected and discussed about by the community in the task's comments section.
Annex: Special Modules and Functionality
Authentication Modules
Also Maintain's authentication system is modular. This means, you can add arbitrary modules to check user credentials for correctness. Maintain comes with three auth modules by default:
- auth_maintain: The default auth module authenticates against the user table in Maintain's database.
- auth_legacy: A derivative of auth_maintain authenticates against an upgraded Maintain 2.4 library and updates the DB to the new password hash on login.
- auth_ldap: This module allows to authenticate against an LDAP server.
The inheritance works as follows:
As usual, the easiest way of figuring out how Maintain authentication works is looking at the Auth modules. Some override parts of PEAR::Auth
, for example.
Search Modules
To integrate your module with the main Maintain search function, you have to override a few more functions from the Module base class. To see which ones and what they are supposed to do, take a look at the module search documentation.
Module Webpages ("Modpages")
Modules can display their own frontend web pages. To accomplish that, you have to make use of a module hook named mod_page. When web_root()/mod_page.php?mod=modname is called from the web page (preferably through somebody clicking a link), the filter method is going to be executed on this particular module (provided it registered to this hook). An empty phphtmllib container is passed to that method. The method can fill up the container arbitrarily and return it afterwards.
Multiple pages can be made by adding another GET variable to the URI (e.g. "page") and switch() ing over $_REQUEST['page'] in the method registered to this hook.
Modpage Example
There is a module with the following methods (among others):
class Display_Weather extends Module {
function register_hooks()
{
Module::add_action('module_dashboard_main', "module_link");
Module::add_filter('mod_page', "page_load");
}
function module_links()
{
$link = html_a(web_root()."mod_page.php?mod=display_weather&page=forecast&zip=97330", "Corvallis, OR");
$cont = container("Useless weather forecast: ", $link);
return $cont;
}
function page_load($cont)
{
switch(strtolower($_REQUEST['page'])) {
case 'forecast':
if ($_REQUEST['zip'] == '97330') {
$cont->add("This is Oregon, so chances are it'll be rainy");
} else {
$cont->add("I don't know, sorry.");
}
break;
default:
header('Location: '.web_root()."index.php");
break;
}
}
}
The module is registered to two hooks. The first one, module_dashboard_main, adds a link to the module widget on the main page. As you can see, the link points to modpage.php?mod=display_weather&page=forecast&zip=97330.
The second hook the module is registered to is the aforementioned filter hook named mod_page which implements a module page.
The user clicks the link.
Now the modpage handler (modpage.php) reads the request (GET and POST) variables and determines that you asked for the display_weather module. It instanciates the module and calls the method you registered to the mod_page hook. Here, the method is called page_load(.) and it takes one argument, $cont.
$cont is an empty phphtmllib container which can be filled with the content that is supposed to be shown on the web page. Here, we just add a single sentence.
You also see here how you can implement several pages: we switch() for the GET variable named "page" and behave differently for every (legal) page. Obviously you want to link to another, private, method for actual pages, so that you don't clutter the switch statement.
Also make sure you have a default statement to account for illegal page choices.