Skip to content

Module Development

Adam Donnison edited this page Oct 31, 2024 · 3 revisions

This section has been written to introduce developers to the dotProject framework and how to use it to write their own module. Bear in mind, the best way to learn about dotProject is to look at the source code as that will always be the best and most up to date resource, but this will at least hopefully point you in the right direction.

If you intend to distribute your module, please adhere to the dotProject's Coding Standard. For more information on how to distribute and announce your module, more information will be included at the end of this article.

Module File Structure

Every module lives in the /modules directory, in a dedicated directory for it. Usually the name of the directory inside is the same as the name of the module. Following is a standard file layout. Essential files are shown in bold.

<dotProject root>/modules/<module_name>/

  • index.php - entry point for the module
  • setup.php - setup procedures to install, uninstall and configure a module
  • <module_name>.class.php - class to represent an item of the module. Usually handles DB store/editing and most common logic
  • addedit.php - edit page
  • do_<module_name>_aed.php - page to handle saving logic. Usually called from the addedit.php page.
  • index_table.php - a common place for a list, to display the module items.
  • <other_module>_tab.<module_name>.php - tab, that will be loaded in <other_module>.
  • index.html - place holder file, to disable directory view on some servers

For example, if you want to build a basic "Hello World" module, you would need these two files:

  • <dotProject root>/modules/hello/setup.php
  • <dotProject root>/modules/hello/index.php

Handling Module Installation and Setup

In order to be able to install a module, you need a setup file, containing a configuration array, describing the module and a setup class, which handles the install/upgrade/uninstall procedures.

Configuration Array

The configuration array is called

$config

It should contain the following indexes:

  • $config['mod_name'] - The name of the module as it would show in the Module Management.
  • $config['mod_version'] - Version of the module. Used to manage upgrade between versions. Make sure you update that after every change to your module
  • $config['mod_directory'] - name of the module directory. This is where dotproject will look for to load the module files.
  • $config['mod_setup_class'] - Name of the setup class, as defined below.
  • $config['mod_type'] - 'user' or 'core' (refering to if the module is a custom one or part of the core functionality). For AddOn modules, always keep that as 'user'
  • $config['mod_ui_name'] - Name of the module as it will be shown in the menus
  • $config['mod_ui_icon'] - Name of the icon to use to represent the module.
  • $config['mod_description'] - Description of the module
  • $config['mod_config'] - whether the module has configuration options (if true, it will show a configure icon in the modules administration)

Additionally, there are a few more settings to help with setting up permissions

  • $config['permissions_item_table'] - name of the database table that will store data
  • $config['permissions_item_field'] - name of the primary key field
  • $config['permissions_item_label'] - name of the field, that best represents the item (title/subject/heading)

Setup Class

The setup class is usually called CSetup<module_name>. It has the following methods:

  • install() - What to do to install the module. Usually handles creating the module DB tables.
  • upgrade($version) - Logic to handle data migration and other procedures to upgrade from previous $version
  • remove() - What to do to uninstall the module. Usually handles removing the module DB tables and any temporary files.
  • configure() - Configuration procedures (if any).

Sample File

setup.php file:

<?php
if (!defined('DP_BASE_DIR')) {
  die('You should not access this file directly.');
}

/**
 *  Name: Hello World
 *  Directory: hello
 *  Version 1.0
 *  Type: user
 *  UI Name: Hello World
 *  UI Icon: ?
 */

$config = array();
$config['mod_name'] = 'Hello World'; // name the module
$config['mod_version'] = '1.0'; // add a version number
$config['mod_directory'] = 'hello'; // tell dotProject where to find this module
$config['mod_setup_class'] = 'CSetupHello'; // the name of the PHP setup class (used below)
$config['mod_type'] = 'user'; //'core' for standard dP modules, 'user' for additional modules from dotmods
$config['mod_ui_name'] = 'Hello World'; // the name that is shown in the main menu of the User Interface
$config['mod_ui_icon'] = 'folder5.png'; // name of a related icon
$config['mod_description'] = 'A Sample Hello World Module'; // some description of the module
$config['mod_config'] = false; // show 'configure' link in viewmods
$config['permissions_item_table'] = 'hello'; // tell dotProject the database table name
$config['permissions_item_field'] = 'hello_id'; // identify table's primary key (for permissions)
$config['permissions_item_label'] = 'hello_text'; // identify "title" field in table


if (@$a == 'setup') {
	echo dPshowModuleConfig($config);
}

// TODO: To be completed later as needed.
class CSetupHello {

  function configure() { return true; }

  function remove() { 
  	$q = new DBQuery();
  	$q->dropTable('hello');
  	$q->exec();
 }
  
  function upgrade($old_version) {
	// Place to put upgrade logic, based on the previously installed version.
	// Usually handled via a switch statement. 
	// Since this is the first version of this module, we have nothing to update.
	return true;
  }

  function install() {
  	$q = new DBQuery();
  	$q->createTable('hello');
  	$q->createDefinition("(
`hello_id` int(11) NOT NULL AUTO_INCREMENT ,
`hello_text` varchar(255) NOT NULL default '',
PRIMARY KEY (`hello_id`) 
) TYPE = MYISAM ");

	$q->exec($sql);
	
	return NULL;
 }
}

Module Code

For any installed module, index.php is the entry point, unless otherwise specified.

The link index.php?m=<module_name> will execute that file.

Any link that includes an action such as index.php?m=<module_name>&a=<action_name> will bypass that file and instead load the <action_name>.php file. For example index.php?m=hello&a=addedit will load the file <dotproject root>/modules/hello/addedit.php.

The index file doesn't have to do much. In fact, you can leave this file empty, and you would still have a working page. The dotProject framework loads all menus, permissions and header/footer of the template you use around it automatically for you.

You can test this by creating an index file which only contains this:

<?php
echo 'Hello World';

Don't forget to check that the file isn't accessed directly (check if DP_BASE_DIR is defined).

if (!defined('DP_BASE_DIR')) {
	die('You should not access this file directly.');
}

As a bare minimum, for it to look like a module, we would also recommend you load the title of the module (via the CTitleBlock class).

$titleBlock = new CTitleBlock('<Module name>', 'folder5.png', $m, "$m.$a");
$titleBlock->show();

The title block can also load bread crumbs, search filters and other items, that are included as standard elements. There are a number of methods to handle this. The most common way to add content to it is $titleBlock->addCell('some text or html');

Global Variables

There are a number of global variables, available for you at this stage:

  • $AppUI - the UI class, including the currently logged in user information.
  • $m - the name of the module (as defined in ?m=...)
  • $a - the name of the action (ad defined in ?a=...)
  • $df - the user's date format
  • ... and many others

Module Class

The module class defines a lot of common logic, that defines the object a module manipulates. It should extend the CDpObject and by doing this, it already inherits a lot of functionality of how to save, update and otherwise manipulate an object, stored in the database.

As this class is also used for database manipulations, it should have all database fields defined as fields of the class.

Some common methods:

  • bind() - populate the object from an array
  • check() - before saving, check that the data is valid. Usually in this method there is code to format the data appropriately for DB storage and if it is not valid, it returns an error, describing the issue(s).
  • store() - store a record in the database
  • delete() - delete the selected record from the database
  • duplicate() - return a copy of the current object, with a new ID

Permissions related methods:

  • canDelete()
  • canEdit()
  • getDeniedRecords()
  • getAllowedRecords()
  • getAllowedSQL()

You don't have to write any of these, as there is default behaviour defined for them in the parent class. The only time you would overwrite them is if you want to define some custom behaviour.

Sample Files

Sample Entry point

  • index.php file
<?php
if (!defined('DP_BASE_DIR')) {
	die('You should not access this file directly.');
}

$AppUI->savePlace();

// retrieve any state parameters
if (isset($_REQUEST['project_id'])) {
	$AppUI->setState('HelloIdxProject', intval($_REQUEST['project_id']));
}

$project_id = $AppUI->getState('HelloIdxProject') !== NULL ? $AppUI->getState('HelloIdxProject') : 0;

if (dPgetParam($_GET, 'tab', -1) != -1) {
	$AppUI->setState('HelloIdxTab', intval(dPgetParam($_GET, 'tab')));
}
$tab = $AppUI->getState('HelloIdxTab') !== NULL ? $AppUI->getState('HelloIdxTab') : 0;
$active = intval(!$AppUI->getState('HelloIdxTab'));

require_once($AppUI->getModuleClass('projects'));

// get the list of visible companies
$extra = array(
	'from' => 'links',
	'where' => 'project_id = link_project'
);

$project = new CProject();
$projects = $project->getAllowedRecords($AppUI->user_id, 'project_id,project_name', 'project_name', null, $extra);
$projects = arrayMerge(array('0'=>$AppUI->_('All', UI_OUTPUT_JS)), $projects);

// setup the title block
$titleBlock = new CTitleBlock('Hello World', 'folder5.png', $m, "$m.$a");
$titleBlock->addCell($AppUI->_('Search') . ':');
$titleBlock->addCell(
        '<input type="text" class="text" SIZE="10" name="search" onChange="document.searchfilter.submit();" value=' . "'$search'" .         'title="'. $AppUI->_('Search in text', UI_OUTPUT_JS) . '"/>'
 ,'',       '<form action="?m=links" method="post" id="searchfilter">', '</form>'
);
$titleBlock->addCell($AppUI->_('Filter') . ':');
$titleBlock->addCell(
	arraySelect($projects, 'project_id', 'onChange="document.pickProject.submit()" size="1" class="text"', $project_id), '',
	'<form name="pickProject" action="?m=hello" method="post">', '</form>'
);
if ($canEdit) {
	$titleBlock->addCell(
		'<input type="submit" class="button" value="'.$AppUI->_('new link').'">', '',
		'<form action="?m=hello&a=addedit" method="post">', '</form>'
	);
}
$titleBlock->show();

$tabBox = new CTabBox('?m=hello', DP_BASE_DIR.'/modules/hello/', $tab);
$tabBox->add('index_table', 'All');
$tabBox->show();

Sample List File

  • index_table.php
$q = new DBQuery();
$q->addQuery('*');
$q->addTable('hello');
$q->addOrder('hello_id');
$q->setLimit(100);
$list = $q->loadList();
?>

<table width="100%" border="0" cellpadding="2" cellspacing="1" class="tbl">
<tr>
	<th nowrap="nowrap">&nbsp;</th>
	<th nowrap="nowrap"><?php echo $AppUI->_('Text');?></th>
</tr>
<?php foreach ($list as $row) { ?>
<tr>
	<td><a href="index.php?m=hello&a=addedit&id=<?php echo $row['hello_id'] ?>">edit</a></td>
	<td><?php echo $row['hello_text'] ?></td>
</tr>
<?php } ?>
</table>

Edit page File

  • addedit.php - edit file:
<?php
if (!defined('DP_BASE_DIR')) {
  die('You should not access this file directly.');
}

$hello_id = intval(dPgetParam($_GET, 'id', 0));
 
// check permissions for this record
$canEdit = getPermission($m, 'edit', $hello_id);
if (!(($canEdit && $link_id) || ($canAuthor && !($hello_id)))) {
	$AppUI->redirect('m=public&a=access_denied');
}

$q = new DBQuery();
$q->addQuery('*');
$q->addTable('hello');
$q->addWhere('hello_id = ' . $hello_id);

// check if this record has dependancies to prevent deletion
$msg = '';
$obj = new CHello();
$canDelete = $obj->canDelete($msg, $hello_id);

// load the record data
$obj = null;
if (!db_loadObject($q->prepare(), $obj) && $hello_id > 0) {
	$AppUI->setMsg('Hello');
	$AppUI->setMsg("invalidID", UI_MSG_ERROR, true);
	$AppUI->redirect();
}

// setup the title block
$ttl = $hello_id ? "Edit" : "Add";
$titleBlock = new CTitleBlock($ttl, 'folder5.png', $m, "$m.$a");
$titleBlock->addCrumb("?m=$m", "list");
$canDelete = getPermission($m, 'delete', $link_id);
if ($canDelete && $link_id > 0) {
	$titleBlock->addCrumbDelete('delete', $canDelete, $msg);
}
$titleBlock->show();

?>
<script language="javascript">
function submitIt() {
	var f = document.uploadFrm;
	f.submit();
}
function delIt() {
	if (confirm("<?php echo $AppUI->_('Delete', UI_OUTPUT_JS);?>")) {
		var f = document.uploadFrm;
		f.del.value='1';
		f.submit();
	}
}
</script>

<table width="100%" border="0" cellpadding="3" cellspacing="3" class="std">

<form name="uploadFrm" action="?m=links" method="post">
	<input type="hidden" name="dosql" value="do_hello_aed" />
	<input type="hidden" name="del" value="0" />
	<input type="hidden" name="hello_id" value="<?php echo $hello_id;?>" />

<tr>
	<td width="100%" valign="top" align="center">
		<table cellspacing="1" cellpadding="2" width="60%">
		<tr>
			<td align="right" nowrap="nowrap"><?php echo $AppUI->_('Text');?>:</td>
			<td align="left"><input type="text" class="text" name="hello_text" value="<?php echo $obj->hello_text;?>"></td>
		</tr>
	</table>
</tr>
<tr>
	<td>
		<input class="button" type="button" name="cancel" value="<?php echo $AppUI->_('cancel');?>" onClick="javascript:if (confirm('<?php echo $AppUI->_('Are you sure you want to cancel?', UI_OUTPUT_JS); ?>')) {location.href = './index.php?m=links';}" />
	</td>
	<td align="right">
		<input type="button" class="button" value="<?php echo $AppUI->_('submit');?>" onclick="submitIt()" />
	</td>
</tr>
</form>
</table>

Sample processing file

  • do_hello_aed.php
<?php
if (!defined('DP_BASE_DIR')) {
  die('You should not access this file directly.');
}

//addlink sql
$hello_id = intval(dPgetParam($_POST, 'hello_id', 0));
$del = intval(dPgetParam($_POST, 'del', 0));

$not = dPgetParam($_POST, 'notify', '0');
if ($not!='0') $not='1';

$obj = new CLink();
if ($link_id) { 
	$obj->_message = 'updated';
} else {
	$obj->_message = 'added';
}

if (!$obj->bind($_POST)) {
	$AppUI->setMsg($obj->getError(), UI_MSG_ERROR);
	$AppUI->redirect();
}

// prepare (and translate) the module name ready for the suffix
$AppUI->setMsg('Hello');
// delete the item
if ($del) {
	$obj->load($hello_id);
	if (($msg = $obj->delete())) {
		$AppUI->setMsg($msg, UI_MSG_ERROR);
		$AppUI->redirect();
	} else {
		if ($not=='1') $obj->notify();
		$AppUI->setMsg("deleted", UI_MSG_ALERT, true);
		$AppUI->redirect("m=hello");
	}
}

if (($msg = $obj->store())) {
        $AppUI->setMsg($msg, UI_MSG_ERROR);
} else {
        $obj->load($obj->hello_id);
        if ($not=='1') $obj->notify();
        $AppUI->setMsg($file_id ? 'updated' : 'added', UI_MSG_OK, true);
}

$AppUI->redirect();

Sample Class File

  • hello.class.php - class file:
<?php
if (!defined('DP_BASE_DIR')) {
	die('You should not access this file directly.');
}

require_once $AppUI->getSystemClass('dp');
/**
 * Hello Class
 */
class CHello extends CDpObject {

	var $hello_id = NULL;
	var $hello_text = NULL;
	
	function CLink() {
		$this->CDpObject('hello', 'hello_id');
	}

	function check() {
	// ensure the integrity of some variables
		$this->hello_id = intval($this->hello_id);

		return NULL; // object is ok
	}

	function delete() {
		global $dPconfig;
		$this->_message = "deleted";

	// delete the main table reference
		$q = new DBQuery();
		$q->setDelete('hello');
		$q->addWhere('hello_id = ' . $this->hello_id);
		if (!$q->exec()) {
			return db_error();
		}
		return NULL;
	}
}

Related Pages