Forum Tables

Just finished deciding on the initial set of tables for the discussion forum.

I wanted to keep things as simple and as flexible as possible. In that regard, I tried to normalize as much as is practical and there is one table for both topics and replies and a seperate table defining Topic > Reply (Parent > Child) relationships. The tags are also assigned by the use of two tables; one for the tag itself and another specifying the relationship.

Since tags double as forums or categories, the tags also have a description provision. I wanted to make anyone browsing a particular tag feel like they’re on a forum page.

Clockwise : Posts table stores topics and replies, PostRelations define parent > child relationships, PostTags define categories, PostTagRelations specify which tags go with which topic

I also wanted to keep track of which authors created which tag. Authors are a unique table where each entry is per post since we want to enable anonymous posting. There is a MemberId field, however, that allows an author to be identified by a registered member profile. Following the same normalization pattern, there are PostAuthors and TagAuthors tables.

Author relationship tables

And, of course, the Members table. I’m still going to be using the MembershipProvider model, but with a few customizations. This table may change in the future, but it does what I need it to do for now.

Members table

And of course, the object models… You may notice from the namespace, I decided to call this project “Road”. Sufficiently vague to be interesting ;)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Road.Models
{
	public enum TopicStatus : byte
	{
		Open, Closed, Hidden, HiddenClosed, InTagPromoted,
		InTagPromotedClosed, Promoted, PromotedClosed
	};

	public enum TagStatus : byte
	{
		Open, Closed, NoAnon, AnonModerated, Moderated
	};

	public enum ReplyStatus : byte
	{
		Open, Hidden
	}

	public class Entity
	{
		public int Id { get; set; }
		public DateTime CreatedDate { get; set; }
		public DateTime LastModified { get; set; }
	}

	public class NamedEntity : Entity
	{
		public string Slug { get; set; }
		public string Name { get; set; }
		public string DisplayName { get; set; }
	}

	public class Creator : NamedEntity
	{
		public string Email { get; set; }
		public string IP { get; set; }
		public string Web { get; set; }
		public string Bio { get; set; }
		public string Avatar { get; set; }
	}

	public class PageEntity : NamedEntity
	{
		public Creator CreatedBy { get; set; }
		public string Summary { get; set; }
		public string Body { get; set; }
	}

	public class Tag : PageEntity
	{
	}

	public class Topic : PageEntity
	{
		public LazyList<Tag> Tags { get; set; }
		public PagedList<Reply> Replies { get; set; }
		public int ViewCount { get; set; }
		public int ReplyCount { get; set; }

		public float? Threshold { get; set; }
		public TopicStatus Status { get; set; }

		public Topic()
		{
			this.Id = 0;
			this.Status = TopicStatus.Open;
			this.ViewCount = 0;
			this.ReplyCount = 0;
			this.Threshold = 0;
		}
	}

	public class Reply : PageEntity
	{
		public int TopicId { get; set; }
		public float? Threshold { get; set; }
		public ReplyStatus Status { get; set; }

		public Reply()
		{
			this.Id = 0;
			Status = ReplyStatus.Open;
			this.Threshold = 0;
		}
	}
}

Note: The LazyList is a lazy loading helper class. There are several examples on the web if you Google the term so I haven’t decided which to use or if I’ll write my own. PagedList is an oldie, but goodie I’ve been using for quite some time. There are many examples of that on the web too.

“Threshold” on both Topic and Reply classes are quality measurements. I want to implement some sort of spam/quality filter that will set the threshold on each as they are entered into the database and allow the user to toggle which range to see. Anonymous users won’t be able to toggle the threshold so any search bots would also be spared any spam.

These few days, I’ve been running all over the place so once I settle down, next update on this, I’ll start posting some actual code.

Advertisements

Discussion Forum mockup

I love Sundays. Not just because there’s no post on Sundays as Vernon Dursley from Harry Potter would infer, but it’s because I get to sleep late some days.

This morning, I put together a quick HTML mockup of the original front page sketch of the discussion forum I talked about a few days ago. All in all, I’m pretty satisfied at how close it came to the Photoshop version and, aside from a few rough edges, I think it accomplishes pretty much everything I want in a “Home” page.

Now I just need to put together the “Tag” page where users can browse all posts under that tag (pretty similar to the “Author” page where users can see all posts under a particular user) and something like a very simple dashboard for the “My Account” page where users can keep track of their subscriptions.

I think it’s best to keep everything organized via feeds for this. If all subscriptions are done in RSS, I think it would be easier for mobile users as well.

I’ll make any changes to the HTML layout on the mockup and start the backend work soon.

Whatever happened to the Discussion Forum?

And when I say “Forum”, I don’t mean Facebook, Twitter or any other social networking site where people stick to their own little walls or flock to hashtags; I mean an actual discussion forum with many topics of interest and people share meaningful views and feedback on said interest. I mean the predecessor to the blog where there were categories for discussion and people posted in coherent sentences and in greater than 140 characters. I mean where you actually got to read an entire category, which means you got to see input from friends, acquaintances and enemies alike.

The bigger your world is, the smaller you feel

Are you part of a community of millions? Then, you know how a single plankton feels. It’s nice to think that your comments will be seen by millions of people, but the reality is that the larger your community, the less of a change there is that any significant number of people will see anything you post unless a moderator or admin sticks it to the front page. Much like how the Freshly Pressed front page works on WordPress.com.

What about the good old days of the internet before spam and porn (the latter is probably debatable for some) when people just posted to discuss topics of interest and share knowledge?

I thought about what it would take to really make a discussion forum usable, compatible with a large community and still feel small and cozy (this might be owing to my liking of small homes). The wheels started turning after I read how a WordPress blog can work nicely as a DIY Case Management System. It’s a brilliant, yet simple, solution to a seemingly complex problem.

First, let’s talk about what we can leave out of a forum.

Categories are dead; Tags are the future

Let’s face it, we don’t just stick to one or two topics of interest these days so it makes sense to be able to browse many at the same time. WordPress already does this and by moving away from categories and into tags, users can have the option of adding multiple tags to their posting options rather than be tied down to one category, theyreby circumventing arbritary limitations on appropriateness (“Your post doesn’t belong here!”). If a topic belongs to none of the existing tags, add your own!

Tags free the user to focus more on what they’re writing rather than where it belongs so the categorization can come from the body of the topic itself. Tags, after all, are a cross-sample of the content.

Groups are not Roles

And promoted or otherwise ostensible roles are silly in most online communities.

This might seem counterintuitive, but think about it: A group may have a set of actions they can perform which means certain groups have certain roles, but groups are not roles themselves. Groups within the same community may also be a source of contention as some people who have been around longer and accumulated more experience may leverage said credit to grow weeds of narcissism. So groups can be a bad thing if that leads to an unwarranted sense self-importance.

Ranks are usually only appropriate when there needs to be a clear reason as to why an admin or moderator needs to show that they’re an admin or mod other than to satiate egos. Breaking down the high walls between staff or authority and ordinary users is a key step in making a community feel like one.

Quicker & Easier to use = More usage

Many moons ago, Joel Spolsky wrote a wonderful treatise on Building Online Communities and while the post is old, the tennets still hold true.

One of the key issues he brings up is the number of steps required in order to post and navigate on a conventional forum. On many of the online communities, the registration, login, password reset and the actual content itself are on disparate locations and most require registration driving away potential contributors. So the trick is to combine most of it into one location without making the place look crowded and have the least number of steps necessary to start posting.

While Joel makes a point on registrations being unnecessary for the most part, if the process is made easy enough, and if it introduces features such as being able to subscribe to many tags at once, registration is a handy way of managing the community. There is, of course, little reason to prevent anonymous posting altogether since every community needs active moderation these days anyway and the majority of mischief can be handled via filters.

Putting it all together

Early this morning, I set about sketching up what would be my ideal discussion forum.

I elected to recycle the HTML5 version of my Simply theme rather than writing something from scratch in order to save time and not reinvent the wheel (and because I’m lazy).

The front page should have all the basics including the latest topics, a tag cloud, a form to add a new topic, a login and registration form. Naturally piling all this onto one page and showing them all at once is confusing, so I elected to use jQuery UI Tabs to organize things. Other than an individual topic view page and a “Home” or “My Account” page, which will serve to aggregate subscribe tags and manage the user profile and subscriptions, 90% of what a forum does can be described in these sketches.

The goal is to be more functional than Twitter while having the same ease of use and less bloated and restrictive than the forums at SomethingAwful.com while having the same capacity to accomodate a large userbase and varied interests.

The front page:

The front page is the "Topics" tab which shows the most recently added topics, the tag cloud and some quick stats. The topic view can look similar to this, possibly with larger text, and without the tag cloud or stats on the side.

Add new topic

The add topic page has a brief description on posting followed by the form. No categories here, only tags.

Login form

The login form shows only what's needed. Both the login and reset forms are together, so there's no jumping through hoops to reset.

Register form

The registration form also shows just the basics. Any more "personal options" would only take away from the community experience and discussion.

And that does it for what I want in a forum. There’s ample room for improvement for sure, but I think this “less is more” approach can help people bring the focus back to the discussion and not about the software itself. Also, the less there is to show, the less there is to fail.

Also note, there are no membership lists. Members will come into the spotlight as they’re posted and as long as they’re active in their tags of choice, they will be noticed and users can browse as many topics that are recently posted or as many topics on each tag(s) they want. No need for something like “Freshly Pressed” or “Readomattic” to get noticed no matter how many posts are made every minute.

I may actually turn this mockup into a working project and add features like author subscriptions, like blog subscriptions we have here, and write it up possibly ASP.Net and MVC when I have the time, but for now, the images will have to do.

Forum core source dump (as of July 31)

I would have finished this a while ago, but I’m in the middle of moving to a new apartment. Until that’s over, I figured I’ll dump what I have so far on the forum.

And I’ve finally settled on a name for this. I shall hereby call this forum script : theForum. Yeah, points for originality there. And of course, this will contain errors, incomplete functions/functionality and other terrible things, so be careful when using any part of this in your own projects. And on top of that the WordPress source code formatting is a bit off making error checking even more vital.

WARNING: This will be a very, very, very long post (7 pages total)!

index.php

/*

	#### theForum : A brand you can trust ####

*/



/******************************************************************************

	Modifying anything below will void the (non-existent) warranty!

*******************************************************************************/

// WARNING: Keep these intact.

// Root location
$rt = dirname(__FILE__) . '/';

// Get the main logic
require_once($rt . 'lib/main.php');

config.php

/******************************************************************************

	Edit your database settings here... (These values go in quotes)

*******************************************************************************/

// Your database server
$presets['db_host'] = 'localhost';

// Database name
$presets['db_name'] = 'wrex';

// Database admin name
$presets['db_user'] = 'wrex_user';

// Database admin password
$presets['db_pass'] = 'password';

// Database type
$presets['db_type'] = 'MySQL';


/******************************************************************************

	Edit your forum settings here... (Some settings require quotes)

*******************************************************************************/


// Forum title (you can include some HTML tags, but they will be stripped for page <title> tags)
$presets['site_title'] = 'the<span>Forum</span>';

// Slogan
$presets['site_slogan'] = 'A brand you can trust';

// Copyright notice
$presets['site_copyright'] = 'Copyright © 2009. All rights reserved.';

// Full site url
$presets['site_url'] = 'http://localhost/';


// Topics per page
$presets['page_topic_limit'] = 15;

// Replies per page
$presets['page_reply_limit'] = 10;


// The relative path to the forums for various paths in themes
$presets['site_path'] = '/';

$presets['site_path_seperator'] = ' » ';

// Present friendly, SEO friendly, URLs by avoiding querystring usage
// (this requires a .htaccess file for Apache or a map/ISAPI filter for IIS)
$presets['site_friendly_urls'] = '1';

// Allow self service creation of accounts. Admins can still create users. (True = 1 / False = 0)
$presets['site_registration'] = '1';

// The default template and styles.
$presets['site_theme'] = 'Section5';

// Closed for maintenance. If set to true, the "Forums closed" message will be displayed
// True = 1 / False = 0
$presets['site_closed'] = '0';

// Forums closed message in HTML format
$presets['site_closed_message'] = '<p>The forums are closed for maintenance. Please check back later.</p>';

// Welcome message (Don't change the "WELCOME" parts). You need to use HTML.
$presets['site_welcome'] =< <<WELCOME



<h2>Welcome
<h3>This is a demonstration template</h3>
<p>Rockwell Accessible is an experimental theme designed to improve web accessibility while maintaining a logical HTML structure. This is one variation of the base theme which is an example of customizability. The idea is to make any changes to the stylesheet while leaving the rest of the HTML intact. It includes provisions for error/notification formatting as well as text and link highlighting.</p>
<p>Read the accompanying <a href="https://eksith.wordpress.com/2009/02/16/rockwell-accessible/">blog post</a>.</p>



WELCOME;

// Footer HTML including copyrights and links etc...
$presets['site_footer'] =< <<FOOTER

	<p>
	<p class="powered">
		<a href="https://eksith.wordpress.com/"><img src="/templates/RockwellGradient/img/ouroboros.png" alt="Design by Eksith" longdesc="https://eksith.wordpress.com/" height="30" width="30" /></a>
	</p>

FOOTER;


/******************************************************************************

	##################### End casual editing #####################
	Edit below (if you know what you're doing) at your own risk!

*******************************************************************************/



// Root location of this file. All file inclusions depend on this.
$presets['site_root'] = dirname(__FILE__) . '/';

// Write enabled directories are required. Please disable execute privileges on these as a security precaution.
$presets['site_upload_directory'] = $presets['site_root'] . 'data/uploads';
$presets['site_cache_directory'] = $presets['site_root'] . 'data/cache';
$presets['site_cache_duration'] = '160';

// The template url is dependent on the SITE_THEME and SITE_TEMPLATE values.

$presets['site_template_directory'] = $presets['site_path'] . 'templates/';
$presets['site_template_url'] = $presets['site_path'] . 'templates/' . $presets['site_theme'] . '/';

// Default language to use in two letter format (preset is English).
$presets['site_lang'] = 'en';


// Security regular expressions
$presets['core_regex_valid_username'] = '/^[\w-_]{4,50}/iu';
$presets['core_regex_valid_displayname'] = '/^[\w\s\.-_]{0,100}/iu';
$presets['core_regex_valid_email'] = '/^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$/i';
$presets['core_regex_valid_querystring'] = '/^[\d-_a-zA-Z]/i';
$presets['core_regex_valid_htmlparams'] = '/^[\w-_]{4,20}/i';
$presets['core_regex_valid_colors'] = '/^#(([a-fA-F0-9]{3}$)|([a-fA-F0-9]{6}$))/i';
$presets['core_regex_invalid_search'] = '/^[~!\|\'()\[\]\< \>;]/i';

// Formatting allowed HTML tags and allowed corresponding attributes

// Convert deprecated tags.
// Convention: array([old tag], [new tag + formatting], [end tag]);
$presets['ui_format_tags_deprecated'] = array(
	array('b', 'strong', 'strong'),
	array('i', 'em', 'em'),
	array('s', 'span style="text-decoration:strikethrough;"', 'span'),
	array('strike', 'span style="text-decoration:strikethrough;"', 'span'),
	array('center', 'div style="text-align:center"', 'div'),
	array('left', 'div style="text-align:left"', 'div'),
	array('right', 'div style="text-align:right"', 'div'),
	array('u', 'span style="text-decoration:underline;"', 'span')
);

// Allowed HTML tags. All others, except for those found above, will be stripped
// Convention: array([tag name], [allowed attributes]);
$presets['ui_format_tags'] = array(
	array('a', 'style,class,title,href,rel,target'),
	array('abbr', 'style,class,title'),
	array('acronym', 'style,class,title'),
	array('blockquote', 'style,class,title,dir,lang,xml:lang'),
	array('br', 'style,class'),
	array('caption', 'style,class,title'),
	array('cite', 'style,class,title'),
	array('code', 'style,class,title'),
	array('del', 'style,class,title'),
	array('em', 'style,class,title'),
	array('h1', 'style,class,title'),
	array('h2', 'style,class,title'),
	array('h3', 'style,class,title'),
	array('h4', 'style,class,title'),
	array('h5', 'style,class,title'),
	array('h6', 'style,class,title'),
	array('hr', 'style,class'),
	array('img', 'style,class,src,height,width,border,hspace,vspace,alt,longdesc'),
	array('ins', 'style,class,title,cite'),
	array('li', 'style,class,title'),
	array('ol', 'style,class,title'),
	array('p', 'style,class,title,align,dir,lang,xml:lang'),
	array('pre', 'style,class,title,dir,lang,xml:lang'),
	array('strong', 'style,class,title'),
	array('span', 'style,class,title,dir,lang,xml:lang'),
	array('table', 'style,class,title,border\-collapse,bgcolor,background'),
	array('tfoot', 'style,class,title,align'),
	array('thead', 'style,class,title,align'),
	array('tbody', 'style,class,title,align'),
	array('td', 'style,class,title,align,valign'),
	array('th', 'style,class,title,align,valign'),
	array('tr', 'style,class,title,align,valign'),
	array('ul', 'style,class,title')
);

// Prohibited keywords in attribute values (from W3C)
// It is STRONGLY recommended that you not remove any of these 
// as certain browsers may allow their execution in unpredictable ways.
$presets['ui_format_invalid_attributes'] = array(
	'onabort',
	'onactivate',
	'onafterprint',
	'onafterupdate',
	'onbeforeactivate',
	'onbeforecopy',
	'onbeforecut',
	'onbeforedeactivate',
	'onbeforeeditfocus',
	'onbeforepaste',
	'onbeforeprint',
	'onbeforeunload',
	'onbeforeupdate',
	'onblur',
	'onbounce',
	'oncellchange',
	'onchange',
	'onclick',
	'oncontextmenu',
	'oncontrolselect',
	'oncopy',
	'oncut',
	'ondataavaible',
	'ondatasetchanged',
	'ondatasetcomplete',
	'ondblclick',
	'ondeactivate',
	'ondrag',
	'ondragdrop',
	'ondragend',
	'ondragenter',
	'ondragleave',
	'ondragover',
	'ondragstart',
	'ondrop',
	'onerror',
	'onerrorupdate',
	'onfilterupdate',
	'onfinish',
	'onfocus',
	'onfocusin',
	'onfocusout',
	'onhelp',
	'onkeydown',
	'onkeypress',
	'onkeyup',
	'onlayoutcomplete',
	'onload',
	'onlosecapture',
	'onmousedown',
	'onmouseenter',
	'onmouseleave',
	'onmousemove',
	'onmoveout',
	'onmouseover',
	'onmouseup',
	'onmousewheel',
	'onmove',
	'onmoveend',
	'onmovestart',
	'onpaste',
	'onpropertychange',
	'onreadystatechange',
	'onreset',
	'onresize',
	'onresizeend',
	'onresizestart',
	'onrowexit',
	'onrowsdelete',
	'onrowsinserted',
	'onscroll',
	'onselect',
	'onselectionchange',
	'onselectstart',
	'onstart',
	'onstop',
	'onsubmit',
	'onunload'
);




/******************************************************************************

	######################## End editing ########################

*******************************************************************************/

// Class loader (required)
function __autoload($class)
{
	global $presets;
	
	// Try and get a class file
	$cfile = $presets['site_root'] . 'lib/' . $class . '.class.php';
	
	// Or module file
	$mfile = $presets['site_root'] . 'modules/' . strtolower($class) . '/module.php';
	
	if(file_exists($cfile))
		require_once($cfile);
	elseif(file_exists($mfile))
		require_once($mfile);
	else
		exit("Unable to Find $class in the library folder or plugin modules folder");
	
	if(!class_exists($class, false))
		exit("Unable to load $class class");
}

PHP Plugin/Module system

After a lot of head scratching and hair pulling, I finally managed to put together something that works for me. Again, this is highly experimental code so feel free to take it apart for errors before using in your projects.

This will be the default module system for the forum script. As such, a lot of the functionality and render regions will need to be modified to fit your own projects.

Warning:

The WordPress sourcecode formatter introduces some odd artifacts and improper formatting. Please review the code for these mistakes. These artifacts/errors are present even in the “view plain” and “copy to clipboard” options.

Update July 31

I’ve made some modifications to this and most of the other sources.

The Modules class:

/**
* Modules class.
* Extending the base forum functionality through pluggable modules.
*
* @author Eksith Rodrigo
* @version 0.1
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @access public
*/

final class Modules
{
	static private $instance;		// Singleton instance for this class
	protected $resources = array();		// Loaded modules
	protected $regions = array();		// Output render regions

	/**
	* Modules constructor. Initiates all given modules
	*
	* @param array $props Properties array global
	* @param object $db Database object
	* @param array $modules List of modules to be loaded
	*/
	private function __construct(&amp;$p, &amp;$db, $modules)
	{
		// Start the regions
		$this->initRegions();

		// Load the modules into the $resources array
		foreach($modules as $resource)
			$this->resources[$resource] = new $resource($p, $db);
	}

	/**
	* Singleton instance.
	*
	* @param array $p The presets in the core passed by reference.
	* @param object $d The database object (MySQL, SQLite et al) passed by reference.
	* @return object Module Singleton instance
	*/
	static function getInstance(&amp;$props, &amp;$db, $modules)
	{
		if(!isset(self::$instance))
			self::$instance = new Modules(&amp;$props, &amp;$db, $modules);

		return self::$instance;
	}

	/**
	* Trigger module events
	* When called within the templates, this function iterates through all loaded modules
	* and calls the appropriate event function.
	*
	* @param string $method Event name ("BeforeHeaderStart", "AfterHeaderEnd" etc...)
	* @param mixed $args This will depend on where the trigger method is called. Generally, will be an array or string.
	*/
	public function __trigger($method, $args)
	{
		// onHeader, onBefore onAfterBody etc... This is the convention used within the module classes.
		$call = "on" . $method;

		// Iterate through each module resource
		foreach($this->resources as $key => $module)
		{
			// If an "on" event function exists for this region...
			if(method_exists($module, $call))
			{
				// Call the function and load the module's output into the region to be injected
				$this->regions[$method] .= call_user_func(array($module, $call), $args);
			}
		}
	}

	/**
	* Gets the module output for a specified region
	*
	* @return string $regions[key] Output HTML from loaded modules
	*/
	public function __render($method)
	{
		return $this->regions[$method];
	}

	/**
	* Template render injection regions.
	* Initiates $regions array into accessible empty keys. Note: Template designers MUST use these keys!
	*/
	private function initRegions()
	{

		/**
		* Generic regions
		*/
		$this->regions["Head"] = '';						// On head tag

		$this->regions["BeforeBody"] = '';					// After body tag
		$this->regions["Body"] = '';						// On body
		$this->regions["AfterBody"] = '';					// Before body end tag

		$this->regions["BeforeHeaderStart"] = '';			// Before header start
		$this->regions["BeforeHeader"] = '';				// After header start
		$this->regions["Header"] = '';						// On header
		$this->regions["AfterHeader"] = '';				// Before header end
		$this->regions["AfterHeaderEnd"] = '';				// After header end

		$this->regions["BeforeContentStart"] = '';			// Before content start
		$this->regions["BeforeContent"] = '';				// After content start
		$this->regions["Content"] = '';					// On content
		$this->regions["AfterContent"] = '';				// Before content end
		$this->regions["AfterContentEnd"] = '';			// After content end

		/**
		* List regions
		*/
		$this->regions["BeforeForumsStart"] = '';			// Before forums list start
		$this->regions["BeforeForums"] = '';				// Before first list item start
		$this->regions["Forums"] = '';						// On forum list as item
		$this->regions["AfterForums"] = '';				// After last forum list item end
		$this->regions["AfterForumsEnd"] = '';				// After forums list end

		$this->regions["BeforeTopicsStart"] = '';			// Before topics list start
		$this->regions["BeforeTopics"] = '';				// Before first topics list item start
		$this->regions["Topics"] = '';						// On topics list as item
		$this->regions["AfterTopics"] = '';				// After last topic list item end
		$this->regions["AfterTopicsEnd"] = '';				// After topics list end

		$this->regions["BeforeRecentStart"] = '';			// Before recent topics list start
		$this->regions["BeforeRecent"] = '';				// Before first topic list item start
		$this->regions["Recent"] = '';						// On topics list as item
		$this->regions["AfterRecent"] = '';				// Before recent topics list end
		$this->regions["AfterRecentEnd"] = '';				// After recent topics list end

		$this->regions["BeforeRepliesStart"] = '';			// Before replies start
		$this->regions["BeforeReplies"] = '';				// After replies start
		$this->regions["Replies"] = '';					// On replies
		$this->regions["AfterReplies"] = '';				// Before replies end
		$this->regions["AfterRepliesEnd"] = '';			// After replies end 

		$this->regions["BeforePagerStart"] = '';			// Before pager start
		$this->regions["BeforePager"] = '';				// After pager start
		$this->regions["Pager"] = '';						// On pager
		$this->regions["AfterPager"] = '';					// Before pager end
		$this->regions["AfterPagerEnd"] = '';				// After pager end 

		/**
		* Individual item regions
		* NOTE: Singular! (no 's' at the end)
		* Content for these regions will be refreshed with each iteration within a loop
		*/
		$this->regions["BeforeForumStart"] = '';			// Before forum item start
		$this->regions["BeforeForum"] = '';				// After forum item start
		$this->regions["Forum"] = '';						// On forum list as item
		$this->regions["AfterForum"] = '';					// Before forum end item
		$this->regions["AfterForumEnd"] = '';				// After forum end item

		$this->regions["BeforeTopicStart"] = '';			// Before topic item start
		$this->regions["BeforeTopic"] = '';				// After topic item start
		$this->regions["Topic"] = '';						// On topics list as item
		$this->regions["AfterTopic"] = '';					// Before topic end item
		$this->regions["AfterTopicEnd"] = '';				// After topic end item

		$this->regions["BeforeLockedTopicStart"] = '';		// Before locked topic item start
		$this->regions["BeforeLockedTopic"] = '';			// After locked topic item start
		$this->regions["LockedTopic"] = '';				// On locked topics list as item
		$this->regions["AfterLockedTopic"] = '';			// Before locked topic item end
		$this->regions["AfterLockedTopicEnd"] = '';		// After locked topic item end

		$this->regions["BeforeStickyTopicStart"] = '';		// Before sticky topic start item
		$this->regions["BeforeStickyTopic"] = '';			// After sticky topic start item
		$this->regions["StickyTopic"] = '';				// On sticky topics list as item
		$this->regions["AfterStickyTopic"] = '';			// Before sticky topic item end
		$this->regions["AfterStickyTopicEnd"] = '';		// After sticky topic item end

		$this->regions["BeforeGlobalTopicStart"] = '';		// Before global sticky topic item start
		$this->regions["BeforeGlobalTopic"] = '';			// After sticky topic item start
		$this->regions["GlobalTopic"] = '';				// On global sticky topic item
		$this->regions["AfterGlobalTopic"] = '';			// Before global sticky topic item end
		$this->regions["AfterGlobalTopicEnd"] = '';		// After global sticky topic item end

		$this->regions["BeforeReplyStart"] = '';			// Before reply item start
		$this->regions["BeforeReply"] = '';				// After reply item start
		$this->regions["Reply"] = '';						// On reply item
		$this->regions["AfterReply"] = '';					// Before reply item end
		$this->regions["AfterReplyEnd"] = '';				// After reply item end

		/**
		* Form data
		*/
		$this->regions["BeforeTopicFormStart"] = '';		// Before new topic form start
		$this->regions["BeforeTopicForm"] = '';			// After new topic form start
		$this->regions["TopicForm"] = '';					// On new topic form
		$this->regions["AfterTopicForm"] = '';				// Before new topic form end
		$this->regions["AfterTopicFormEnd"] = '';			// After new topic form end

		$this->regions["BeforeReplyFormStart"] = '';		// Before new reply form start
		$this->regions["BeforeReplyForm"] = '';			// After new reply form start
		$this->regions["ReplyForm"] = '';					// On new reply form
		$this->regions["AfterReplyForm"] = '';				// Before new reply form end
		$this->regions["AfterReplyFormEnd"] = '';			// After new reply form end

		$this->regions["BeforeLoginFormStart"] = '';		// Before login form start
		$this->regions["BeforeLoginForm"] = '';			// After login form start
		$this->regions["LoginForm"] = '';					// On login form
		$this->regions["AfterLoginForm"] = '';				// Before login form end
		$this->regions["AfterLoginFormEnd"] = '';			// After login form end

		$this->regions["BeforeReigsterFormStart"] = '';	// Before register form start
		$this->regions["BeforeReigsterForm"] = '';			// After register form start
		$this->regions["RegisterForm"] = '';				// On register form
		$this->regions["AfterRegisterForm"] = '';			// Before register form end
		$this->regions["AfterRegisterFormEnd"] = '';		// After register form end

		$this->regions["BeforeSearchStart"] = '';			// Before search form start
		$this->regions["BeforeSearch"] = '';				// After search form start
		$this->regions["Search"] = '';						// On search form
		$this->regions["AfterSearch"] = '';				// Before search form end
		$this->regions["AfterSearchEnd"] = '';				// After search form end

		/**
		* Individual form fields
		*/
		$this->regions["SearchFieldStart"] = '';			// Before search input field start
		$this->regions["SearchFieldEnd"] = '';				// After search input field end

		$this->regions["UsernameFieldStart"] = '';			// Before login username input field start
		$this->regions["UsernameFieldEnd"] = '';			// After login username input field end

		$this->regions["PasswordFieldStart"] = '';			// Before login password input field start
		$this->regions["PasswordFieldEnd"] = '';			// After login password input field end

		$this->regions["TopicTitleFieldStart"] = '';		// Before topic title input field start
		$this->regions["TopicTitleFieldEnd"] = '';			// After topic title input field end

		$this->regions["TopicContentFieldStart"] = '';		// Before topic content textarea field start
		$this->regions["TopicContentFieldEnd"] = '';		// After topic content textarea field end

		$this->regions["TopicForumFieldStart"] = '';		// Before topic forum select field start
		$this->regions["TopicForumFieldEnd"] = '';			// After topic forum select field end

		$this->regions["ReplyContentFieldStart"] = '';		// Before reply content textarea start
		$this->regions["ReplyContentFieldEnd"] = '';		// After reply content textarea end

		/**
		* These fields will only be present if the user is anonymous.
		* Registered users will have the username called from their login information.
		*/
		$this->regions["TopicNameFieldStart"] = '';		// Before topic author name input field start
		$this->regions["TopicNameFieldEnd"] = '';			// After topic author name input field end

		$this->regions["TopicEmailFieldStart"] = '';		// Before topic author email input field start
		$this->regions["TopicEmailFieldEnd"] = '';			// After topic author email input field end

		$this->regions["ReplyNameFieldStart"] = '';		// Before reply author name input field start
		$this->regions["ReplyNameFieldEnd"] = '';			// After reply author name input field end

		$this->regions["ReplyNameFieldStart"] = '';		// Before reply author email input field start
		$this->regions["ReplyNameFieldEnd"] = '';			// After reply author email input field end

		/**
		* Navigation regions and items
		*/
		$this->regions["BeforeBreadcrumbStart"] = '';		// Before breadcrumb links start
		$this->regions["BeforeBreadcrumb"] = '';			// After breadcrumb links start
		$this->regions["Breadcrumb"] = '';					// On breadcrumb
		$this->regions["AfterBreadcrumb"] = '';			// Before breadcrumb end
		$this->regions["AfterBreadcrumbEnd"] = '';			// After breadcrumb end

		$this->regions["BeforeCrumbItemStart"] = '';		// Before breadcrumb link item start
		$this->regions["BeforeCrumbItem"] = '';			// After breadcrumb link item start
		$this->regions["CrumbItem"] = '';					// On breadcrumb item
		$this->regions["AfterCrumbItem"] = '';				// Before breadcrumb item end
		$this->regions["AfterCrumbItemEnd"] = '';			// After breadcrumb item end

		$this->regions["BeforeNavigationStart"] = '';		// Before navigation list start
		$this->regions["BeforeNavigation"] = '';			// After navigation list start
		$this->regions["Navigation"] = '';					// On navigation list as list item
		$this->regions["AfterNavigation"] = '';			// After last navigation list item
		$this->regions["AfterNavigationEnd"] = '';			// After navigation list end

		$this->regions["BeforeNavItemStart"] = '';			// Before navigation item start
		$this->regions["BeforeNavItem"] = '';				// After navigation item start
		$this->regions["NavItem"] = '';					// On navigation list item
		$this->regions["AfterNavItem"] = '';				// Before navigation list item end
		$this->regions["AfterNavItemEnd"] = '';			// After navigation list item end

		/**
		* Ancillary regions
		* Sidebar, footer etc...
		*/
		$this->regions["BeforeSidebarStart"] = '';			// Before sidebar start
		$this->regions["BeforeSidebar"] = '';				// After sidebar start
		$this->regions["Sidebar"] = '';					// On sidebar
		$this->regions["AfterSidebar"] = '';				// Before sidebar end
		$this->regions["AfterSidebarEnd"] = '';			// After sidebar end

		$this->regions["BeforeSideWidgetStart"] = '';		// Before sidebar widget item start
		$this->regions["BeforeSideWidget"] = '';			// After sidebar widget item start
		$this->regions["SideWidget"] = '';					// On sidebar widget
		$this->regions["AfterSideWidget"] = '';			// Before sidebar widget end
		$this->regions["AfterSideWidgetEnd"] = '';			// After sidebar widget end

		$this->regions["BeforeSideItemStart"] = '';		// Before sidebar widget item start (module specific)
		$this->regions["BeforeSideItem"] = '';				// After sidebar widget item start
		$this->regions["SideItem"] = '';					// On sidebar widget item
		$this->regions["AfterSideItem"] = '';				// Before sidebar widget item end
		$this->regions["AfterSideItemEnd"] = '';			// After sidebar widget item end

		$this->regions["BeforeFooterStart"] = '';			// Before footer start
		$this->regions["BeforeFooter"] = '';				// After footer start
		$this->regions["Footer"] = '';						// On footer
		$this->regions["AfterFooter"] = '';				// Before footer end
		$this->regions["AfterFooterEnd"] = '';				// After footer end
	}
}

The Module base class (all modules must inherit this) :

/**
* Module class.
* Sets the default module functionality. All modules must inherit this class to be usable.
*
* @author Eksith Rodrigo
* @version 0.1
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @access public
*/

class Module
{
	protected $props;			// Stored properties passed by main class
	protected $database;		// Database object

	/**
	* Module constructor. Sets the database and presets.
	*
	* @param array $p Properties passed by Modules class (which in turn gets it from global configurations)
	* @param object $db Database object
	*/
	protected function __construct(&amp;$p, &amp;$db)
	{
		$this->props = $p;
		$this->database = $db;
	}

	/**
	* Set properties
	*
	* @param string $name Property name to modify
	* @param mixed $value New property value
	*/
	protected function __set($name, $value)
	{
		// No "isset" check as new values may be shared between modules.
		$this->props[$name] = $value;
	}

	/**
	* Get properties
	*
	* @param string $name Property name to get
	* @return mixed $props[$name] Property value, if it exists.
	*/
	protected function __get($name)
	{
		if(isset($this->props[$name]))
			return $this->props[$name];

		return null;
	}

	/**
	* Helper function to check if a property value exists.
	*
	* @param string $name Property name to check
	* @return bool true If the property exists or returns false.
	*/
	protected function __isset($name)
	{
		return isset($this->props[$name]);
	}

	/**
	* Helper function to delete a property value.
	*
	* @param string $name Property value to delete.
	*/
	protected function __unset($name)
	{
		if(isset($this->props[$name]))
			unset($this->props[$name]);
	}

	/**
	* Helper function to include custom JavaScript or CSS files.
	*
	* @param string $file Path to include (depends on preset globals passed to modules)
	* @param string $type File type. "js" For JavaScript or "css" for stylesheets.
	* @return string $path Formated <script> or <link> tags for JS or CSS extras.
	*/
	protected function libInject($file, $type)
	{
		$path = '';
		switch(strtolower($type))
		{
			case "js":
				$path = '<script type="text/javascript" src="'. $file .'"></script>';
				break;
			case "css":
				$path = '<link rel="stylesheet" href="'. $file .'" />';
				break;
		}

		return $path;
	}
}

An example module. In this case a WYSIWYG control based on Paul James’ simple but elegant script.

Note: The modules are placed within the /modules folder of your root directory. In my case, the root path is a preset global. You should use your own system for this.

/**
* WYSIWYG (What You See Is What You Get) editor plugin.
* Uses JavaScript by Paul James (licensed under BSD). Inherits "Module".
*
* @author Eksith Rodrigo.
* @version 0.1
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @link http://www.peej.co.uk/sandbox/wysiwyg/ Paul James
* @access public
*/

final class Wysiwyg extends Module
{
	/**
	* Module constructor. Sets the database and presets.
	* This particular example doesn't use the database object.
	*
	* @param array $p Properties passed by Modules class (which in turn gets it from global configurations)
	* @param object $db Database object
	*/
	public function __construct(&amp;$p, &amp;$db)
	{
		parent::__construct($p, $db);
	}

	/**
	* Set properties
	*
	* @param string $name Property name to modify
	* @param mixed $value New property value
	*/
	public function __set($name, $value)
	{
		if(isset($name) &amp;&amp; isset($value))
			parent::__set($name, $value);
	}

	/**
	* Get properties
	*
	* @param string $name Property name to get
	* @return mixed $props[$name] Property value, if it exists.
	*/
	public function __get($name)
	{
		return parent::__get($name);
	}

	/**
	* Helper function to check if a property value exists.
	*
	* @param string $name Property name to check
	* @return bool true If the property exists or returns false.
	*/
	public function __isset($name)
	{
		return parent::__isset($name);
	}

	/**
	* Helper function to delete a property value.
	*
	* @param string $name Property value to delete.
	*/
	public function __unset($name)
	{
		parent::__unset($name);
	}

	/**
	* onHead event (see Modules class for a complete list of events).
	* Injects the css class and JavaScrpt file for WYSIWYG
	*
	* @param mixed $args This depends on where the event is called.
	* @return string $html Rendered output to be placed within regions.
	*/
	public function onHead(&amp;$args)
	{
		// Load CSS
		$html = parent::libInject($this->props["site_path"] . "modules/wysiwyg/wysiwyg.css", "css") . "\n";

		// Load JavaScript
		$html .=  "\n". parent::libInject($this->props["site_path"] . "modules/wysiwyg/wysiwyg.js", "js") . "\n";
		return $html;
	}
}

This is an example of how to use this script. I initiated the modules (Note, only the Wysiwyg module is being loaded in this example) :

$modules = Modules::getInstance($presets, $database, array("Wysiwyg"));

And use this helper function to both trigger an event within the modules and get it’s rendered output

function triggerRender($method)
{
	global $modules;
	$modules->__trigger($method, array($forums_list, $topics_list, $replies_list));
	return $modules->__render($method);
}

In my forum script, I placed regular code files within a /lib folder. But I placed all modules within their own folders inside a /modules folder.

I.E. The WYSIWYG would be installed as follows :

/modules/wysiwyg

All modules must contain a “module.php” file which is the primary class file for that module.

To load everything during runtime, I used the autoload functionality. Note: All directory names and module file names are lowercase. Only the class names (in the /lib folder) uses mixed case.

function __autoload($class)
{
	global $presets;

	// Try and get a class file
	$cfile = $presets['site_root'] . 'lib/' . $class . '.class.php';

	// Or module file
	$mfile = $presets['site_root'] . 'modules/' . strtolower($class) . '/module.php';

	if(file_exists($cfile))
		require_once($cfile);
	elseif(file_exists($mfile))
		require_once($mfile);
	else
		exit("Unable to Find $class in the library folder or plugin modules folder");

	if(!class_exists($class, false))
		exit("Unable to load $class class");
}

All modules are called within the templates. A specific region, E.G. “Head” should trigger the “onHead” method within all modules which have this event.

This is an example of a template file (very similar to what I’ve written for the forum templates) :

// Begin the HTML header
// Note: $tpl is the output variable to store all the HTML. I figured this would be easier to manage.
$tpl =< <<HTML_HEADER
<?xml version="1.0" encoding="UTF-8" ?>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>{$presets['site_title']}</title>
	<link rel="stylesheet" href="{$presets['site_template_url']}style.css" />
	<link rel="stylesheet" href="{$presets['site_template_url']}colors.css" />
HTML_HEADER; // End first part of the header

// Trigger and get the output for the "Head" region from the modules
$tpl .= triggerRender("Head");

// Finish off the header
$tpl .=< <<HTML_HEADER
</head>
<body dir="ltr">
HTML_HEADER;

Update

In retrospect, the above example of a template is really ugly.

The following is a far more elegant method of calling functions within heredocs…

// Create function method for rending regions
$render = create_function('$region', 'return triggerRender($region);');

// Begin template header
$tpl =< <<HTML_HEADER
<?xml version="1.0" encoding="UTF-8" ?>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>{$presets['site_title']}</title>
	<link rel="stylesheet" href="{$presets['site_template_url']}style.css" />
	<link rel="stylesheet" href="{$presets['site_template_url']}colors.css" />
	{$render("Head")}
</head>
<body dir="ltr">
	{$render("BeforeBody")}