Plugin Architecture for CodeIgniter
I have finally started development of Threadler - my uber new discussion forum system! I have stalled a lot on this project mainly because I couldn’t decide which framework (if any) to use for development. I have been using CodeIgniter for the past 2 or 3 years but I was tempted by Kohana (a CI spin-off) and the prestige of building my own framework.
But in the end, I settled with CodeIgniter as I am pretty sure (for now) that I can make it do everything that I want it to.
So the first stumbling block was building the software to allow the use of plugins (a.k.a modules, extensions or add-ons depending on your favourite buzz word). Plugins will be used to extend the functionality of Threadler. For example, the bare-bones installation will not have Private Messaging installed, instead you can add this as a pre-built module and voila!
My plugins architecture must cater for:
- Hooks/events/triggers to add specified code into the existing Threadler code base
- All new pages (made up of controllers, models and views) which the new module may need
For example, a Private Messaging module may use a hook to add in the “Private Message” link within the normal forum display. But then it also requires new pages to display the form required to send/receive private messages.
Anyway, here is what I have come up with so far, remember it is still a work in progress. I have basically used a combination of the elements below:
- The hooks/triggers/events concept from 68kb project
- The modular development concept from the Modular Extensions HMVC CI plugin
Disclaimer: The code below is proof of concept stuff only! I have not fully tested or tried to extend this to any degree. Also, I have not yet implemented the administration section to activate/deactivate a module - this will be another blog post.
Module Requirements
For it all to work, the end result is that modules must have the following things:
- Be contained within a folder within /system/application/modules/ (e.g. /system/application/modules/private_messaging/)
- Should contain:
- module_info.php file - contains information about the module (e.g. author, name of module etc)
- activate.php (optional) - contains code which is run when the module is activated in an administration section (e.g. sql statements to create private messaging tables)
- deactivate.php (optional) - contains code which is run when the module is deactivated
- events.php (optional) - registers which triggers/hooks the module requires
- controllers (optional) - used if the module requires separate files (as per a normal CI Controller). All controllers must extend the Modules_Controller (see later for reasoning).
- views (optional) - used if the module requires separate view files (as per normal CI View files)
- models (optional) - used if the module requires separate model files (as per normal CI Model files)
Module Example Code
Here is a very basic test module which can be found in /system/application/modules/test/. This module does not require any activate.php or deactivate.php file because it pretty much does nothing except echo out some wonderful text.
The module_info file:
1 2 3 4 5 6 7 8 | <?php $data['module']['name'] = "test"; $data['module']['displayname'] = "Test Module"; $data['module']['description'] = 'Module which does very little'; $data['module']['version'] = "1.0.0"; $data['module']['author'] = "Leeane"; $data['module']['homepage'] = "http://blog.leeane.com"; ?> |
The events file:
<?php class test_events { function test_events(&$threadler_events) { $threadler_events->register('testing', $this, 'my_test'); $threadler_events->register('testing', $this, 'my_test2'); } function my_test() { echo 'This is a test of an event trigger<br />'; } function my_test2() { echo 'This is yet another test of an event trigger<br />'; } } ?>
The example controller (accessible by http://mysite.com/test/):
1 2 3 4 5 6 7 8 9 10 11 | <?php class Test extends Modules_Controller { function Test() { parent::Modules_Controller(); } function index() { $this->load->view('welcome_message'); } } |
The example view file:
1 2 3 4 5 | <html> <head><title>Welcome to CodeIgniter</title></head> <h1>Testing the Modules thingo!</h1> Now lets test the output of a trigger/event/hook thingo! <?php $this->threadler_events->trigger('testing');?> |
OK thats enough of the module itself, onto the library code.
Libraries
First up, the events and hooks library (this is largely taken from the 68Kb project - I encourage you to look at the relevant project page at Google Code):
/system/application/libraries/Threadler_events.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | <?php if (!defined('BASEPATH')) exit('No direct script access allowed'); /** * Threadler Events * * This is largely based on the Kb_events class authored by Eric Barnes (68Kb). * * @author Eric Barnes * @link http://www.68kb.com/hooks.html */ class Threadler_events { /** * @var array Array of registered hooks and their listners */ var $listeners = array(); /** * Threadler events * * Allow users to extend the system. * Idea from Iono */ function threadler_events() { $data=''; $CI =& get_instance(); //if($CI->db->table_exists('modules')) //{ // $CI->db->from('modules'); // $CI->db->where('active', '1'); // $query = $CI->db->get(); // foreach ($query->result() as $row) // { if (@file_exists(APPPATH .'modules/test/events.php')) { include_once(APPPATH .'modules/test/events.php'); $class = 'test_events'; if (class_exists($class)) { new $class($this); } } // } //} } /** * Register a listner for a given hook * * @param string $hook * @param object $class_reference * @param string $method */ function register($hook, &$class_reference, $method) { // Specifies a key so we can't define the same handler more than once $key = get_class($class_reference).'->'.$method; $this->listeners[$hook][$key] = array(&$class_reference, $method); } /** * Trigger an event * * @param string $hook * @param mixed $data */ function trigger($hook, $data='') { $call_it=''; // Are there any hooks? if (isset($this->listeners[$hook]) && is_array($this->listeners[$hook]) && count($this->listeners[$hook]) > 0) { // Loop foreach ($this->listeners[$hook] as $listener) { // Set up variables $class =& $listener[0]; $method = $listener[1]; if(method_exists($class,$method)) { // Call method dynamically $call_it.=$class->$method($data); } } } return $call_it; } } |
Please note, you would want to uncomment out the database calls in the file above - I took them out just to get this proof of concept working. I have hard-coded this to work with the “test” module only for now. Please also note that for this all to work you will need to autoload the “threadler_events” library by adding $this->load->library(’threadler_events’); in your /system/application/config.php file.
/system/application/libraries/Threadler_hooks.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | <?php if (!defined('BASEPATH')) exit('No direct script access allowed'); /** * Threadler Hooks * * This is largely based on the Kb_hooks class authored by Eric Barnes (68Kb). * * @author Eric Barnes * @link http://www.68kb.com/hooks.html */ class Threadler_hooks { /** * @var array All User modules * @access private */ var $user_modules; var $listeners = array(); function Threadler_hooks() { } // ------------------------------------------------------------------------ /** * Test if a function exists * * @access private * @param string the function name * @return bool */ function _test_exists($function_name) { return function_exists($function_name); } // ------------------------------------------------------------------------ /** * Invoke the hook * * This function actually invokes the hook. * It is used so two modules can share one hook. * * @access private * @param string the function name * @param array the function params * @return bool */ function _invoke_hook($function_name, $params) { return call_user_func($function_name, $params); } // ------------------------------------------------------------------------ /** * Call the hook * * Throughout the script when a certain action * occurs it will call this hook. * * @access public * @param string the function name * @param array the function params * @return bool */ function call_hook($function, $params='') { // Find all the user modules $data=''; $CI =& get_instance(); $CI->load->helper('url'); //$CI->db->from('modules'); //$CI->db->where('active', '1'); //$query = $CI->db->get(); //echo $CI->db->last_query(); //foreach ($query->result() as $row) //{ if (@file_exists(APPPATH .'modules/test/hooks.php')) { require_once(APPPATH .'modules/test/hooks.php'); } $function_name = 'test_'.$function; if ($this->_test_exists($function_name)) { $data.=$this->_invoke_hook($function_name, $params); } //} return $data; } } |
Please note, you would want to uncomment out the database calls in the file above - I took them out just to get this proof of concept working. I have hard-coded this to work with the “test” module only for now.
Next you want to install the Modular Extensions HMVC plugin - basically you just need to drop the Modules.php, Controller.php and MY_Router.php files into the /system/application/libraries/ folder and you’re all done.
Lastly, I do not want people to be able to access a modules controllers directly unless the module has been activated in the administration section. Thankfully, the hooks/events will not trigger unless the module is activated (thanks to 68Kb project’s code, although those relevant bits were commented out in this example). But people will be able to directly access http://mysite.com/test/ and view the test controller which is probably not a good idea!
So for this, we want to make a new Controller class which all modules must inherit from.
I.e. class MyNiftyModule extends Module_Controller {…}
The module can be tested for conformity to this rule at the point a module is added to the library of available modules on the threadler website - I haven’t got code for this yet. This new Module_Controller will be called before any module’s controller is run, and will check (thanks to a database call) if the relevant module is activated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php if (!defined('BASEPATH')) exit('No direct access allowed'); /** * Modules_Controller * * * Description: * This library is used in all modules for Threadler (via "class ModuleName extends Modules_Controller {...}") * It checks if a given module is "activated" in the database before displaying any content. * * This is used to prevent anyone from directly accessing an unactivated modules' controllers * */ class Modules_Controller extends Controller { function Modules_Controller() { parent::Controller(); $CI =& get_instance(); //grab modules name (this will be the folder under /threadler/modules/) $module_name = $CI->uri->segment(1); //query the database to determine if the module is activated $query = $CI->db->query("SELECT 1 FROM threadler_modules WHERE module_name = ".$CI->db->escape($module_name)." AND enabled = 'yes'"); $num_results = $query->num_rows(); if ($num_results != 1) { //stop everything! show_error('Uh-oh! This module has not been activated in the threadler administration section.'); } else { //do nothing, allow the controller to load as per normal } } } |
I am sure you can get the gist for the kind of SQL table you need for this to function properly.
Testing
When we go to http://mysite.com/test/, we get:
Testing the Modules thingo!
Now lets test the output of a trigger/event/hook thingo!
This is a test of an event trigger
This is yet another test of an event trigger
Conclusions
It all seems to work fairly well so far. There’s obviously more work to do with actually activating the module in the admin section and maybe a priority setting for a modules hook etc but I am fairly happy with what I have so far (all be it a quick throw-together).
I am a bit new to plugins so if you see any issues with the architecture or code I have posted please let me know. I am interested to see how it could be improved.
