Wednesday, June 25, 2008

Do It Yourself - Plugins (PHP Tutorial)

Today I would like to share with you a quick way to have a plug-in engine running at your site. I came up with this concept quickly a few days ago when I needed to make an extensible layer for one of our projects.

Today we are going to achieve some fundamental functionality of a plug-in system. Please note that any of the external classes used in this tutorial's code can be replaced with only a few lines of code. I was too lazy to do so while copy-pasting my code. And of course you can comment and ask for help.

First of all, let's define the functionality we expect from our system:

  • The plugin creator shouldn't have to touch any code other than his plugin
  • There should be minimal installation needed
  • The plugin should be standalone and deployable to any system using our framework without any hassle


Sounds hard? It isn't. First of all, let me walk you through a few key concepts we are going to exercise while creating this system. I am basing all of this on the presumption that you already are familiar with object oriented programming, if you are not, I advise you to fill in the gaps

Static Keyword


So, you are probably familiar with classes and objects, and you know that to use a class, you first must instantiate it using the new keyword.

You also probably know that every time you create an object of the class' type, you are creating all the methods and properties, and they belong to that specific object.

Basically, when you put the keyword static before a property or method definition, you are declaring that this method or property will belong to the class type itself, rather than instances of it (objects), and therefore be common to all the instances of the same class, and be callable without even creating an instance of a class

Let me give you a quick example.

class className{
static private $variable = 25;
function get_variable(){
return className::$variable;
}

function __construct( $inputNumber = null ){

if(!is_null( $inputNumber )){
className::$variable = $inputNumber;
}
}
}

// We create the first instance. Since we don't give a new value, it stays 25, like
// in the initialization
$firstInstance = new className();
// We create a second instance, this time we change the variable to 45
$secondInstance = new className(45);
// We create a third instance
$thirdInstance = new className();


// Since the variable is static, the change we did in the second function affects
// all of them, because the variable belongs to the class itslef, and not a separate
// instance of it
// All the three functions will output: 45
echo $firstInstance->get_variable();
echo $secondInstance->get_variable();
echo $thirdInstance->get_variable();


Now that you know what the static keyword is, let us move to the next thing you need to know before attempting to write a plugin engine.

Dynamic Access & Creation


This is a very controversial feature in PHP. And some will argue (and will be right in a way) that it shouldn't be there (its just like the argument about strongly typed and weakly typed languages). But since the feature is there, why not make use of it?

Simply enough, PHP lets you call functions and classes dynamically. For instance, you can dynamically decide which class to create.

$variable = 'myClass';
$myObject = new $variable();

The code above will create an instance of the class 'myClass'.

Also, PHP lets us call functions and methods dynamically.

$functionName = 'multiply';
// Same as doing: multiply( 5, 2 );
$functionName( 5, 2 );

// Same as doing: multiply( 5, 2 );
call_user_func( 'multiply', 5, 2 );

// Same as doing: $myObject->myMethod( 'some text' );
call_user_func( array( 'myObject', 'myMethod' ), 'some text' );

// Same as doing: myStaticClass::myMethod( 'some text' );
call_user_func( array( 'myStaticClass', 'myMethod' ), 'some text' );


Let's Get Started


Oh, I almost forgot, we were going to build a plugin engine... Well, let's get started.

First of all, before writing any code, I always figure out what I'm going to make. Then we figure out how, and then after I have everything in mind, I start writing some actual code.

The first planning stage is pretending to be a user of our to-be-written module, and making a quick list of how the final product will act.

So for our plugin system it will be:

  • All the plugins reside in a plugins/ subdirectory of our website. Each in it's own directory.
  • There will be a class that handles all the plugins, and registers them to the system.
  • A plugin can be turned off.
  • The plugin installation should be as simple as copying it to the plugins/ directory, and turning it on.


Then, after some thinking, I decided that I will go with this kind of code architecture:

  • Plugin engine is a fake-singleton class (Read some stuff about actual singleton classes. By fake-singleton, I mean that it is an absolutely static class of which you can't create instances, but it isn't a traditional singleton class (which actually has one instance of a class)
  • Each plugin is also a static class. Why did I choose static classes? Code readability and aesthetics mainly. As well as ease of implementation.
  • Each class has a corresponding table entry in MySQL, that tells us if we enabled the plugin or not.
  • There are multiple "hook checkpoints" where plugins can hook in their code.


Plugin Engine


Basically, what I'll do next here is give the final code, copy-pasted right off my application, and then explain what needs explanation. Please note that you can't just copy-paste my code as it won't work right away. You will need to replace a few lines of code. Nothing too bad.

plugin.class.php
This is the heart of the plugin system. It checks the plugins directory for any plugins, validates them with our database, and then registers them to be ran at the hook checkpoints.

// This bit checks if we are currently
// running inside our website. It prevents
// from overly curious people to launch
// this php file from outside of our script
if( !defined( "INPROCESS" ) ){
header("HTTP/1.0 403 Forbidden");
die();
}

// Those two are classes that I wrote
// to simplify access to mysql and other
// databases. You will have to delete those
// two lines, and write your own code
// to access mysql
require_once( 'data.class.php' );
require_once( 'data.mysql.class.php' );


// This is the parent class for all plugins
// It contains a private constructor, that
// basically makes sure we won't have any
// instances of our static classes. Making
// them completely static.
class plugin{
private function __construct(){}
}

// This is the actual plugin class.
class pluginClass{
// This will be the list of active plugins
static private $plugins = array();

// Again, we don't want any instances
// of our static class.
private function __construct(){}

static function initialize(){
// I have those variables elsewhere in
// a config.php file. you can replace
// those with your own
global $config_fullpath;
global $config_username;
global $config_password;
global $config_server;
global $config_database;

$list = array();
// Populate the list of directories to check against
if ( ($directoryHandle = opendir( $config_fullpath . '/plugins/' )) == true ) {
while (($file = readdir( $directoryHandle )) !== false) {
// Make sure we're not dealing with a file or a link to the parent directory
if( is_dir( $config_fullpath . '/plugins/' . $file ) && ($file == '.' || $file == '..') !== true )
array_push( $list, $file );
}
}


// Get the plugin list from MySQL. Note that you will have to replace
// this code with your own, since I'm using classes that I didn't
// include in this article. Fortunately, it won't be too much of a problem.

// Connect to mysql
$mysqlConnection = new mysqlConnection( $config_username, $config_password, $config_server, $config_database );
// We select all the plugins from our database
// Each plugin has it's name stored, and whether
// it is active or not (active = 0 or 1)
$mysqlConnection->prepareQuery( 'SELECT * FROM plugins' );
$results = $mysqlConnection->executeQuery();
$results = $mysqlConnection->resultToObjects( $results );
$newResult = array();



// Create an array: 'plugin name' = 'active' (1 or 0)
foreach ( $results as $result ){
$newResult[$result->name] = $result->active;
}

// Register the active plugins
foreach( $list as $plugin ){
if($newResult[$plugin] == "1"){
pluginClass::register( $plugin );
}
}
}

// Hook the active plugins at a checkpoint.
// You will see exactly how it works later on.
static function hook( $checkpoint ){
// Cycle through all the plugins that are active
foreach(pluginClass::$plugins as $plugin){
if(!call_user_func( array( $plugin, $checkpoint ) ))
// Throw an exception if we can't hook the plugin
throw new Exception( "Cannot hook plugin ($plugin) at checkpoint ($checkpoint)" );
}
}

// Registration adds the plugin to the list of plugins, and also
// includes it's code into our runtime.
static function register( $plugin ){
global $config_fullpath;
require_once( $config_fullpath . "/plugins/$plugin/$plugin.class.php" );
array_push( pluginClass::$plugins, $plugin );
}
}


index.php

// Tell the class it is safe to run
define( "INPROCESS", true );

// My configuration file
require_once( './config.php' );
// Our plugin class
require_once( './include/plugin.class.php');

// Initialize our plugin engine
pluginClass::initialize();
// Create a new hook point called "onLoad"
// Now all the plugins that can be hooked here
// will be ran.
pluginClass::hook( "onLoad" );


A Sample Plugin
A sample plugin will reside in it's own directory inside plugins/. For example plugins/helloworld/. Then we need to go (or have a script that does it automatically. Homework for you...) to our database and add it, with its name there and active = 1. That's it, we're done with installing our plugin!

plugins/helloworld/helloworld.class.php


if( !defined( "INPROCESS" ) ){
header("HTTP/1.0 403 Forbidden");
die();
}
// We inherit the parent class plugin to make sure we will have no accidental instances of this class.
class helloworld extends plugin {
// That's right. We create a function with the name
// of our hook, and walla! We have a plugin hooked at
// onLoad.
static function onLoad(){
print 'Hello World!';
return true;
}
}


That's it. I am sorry for not doing this one step by step, but if you have any questions, you are welcome to comment. Of course, I didn't include the whole code here. You can create an installer that installs classes automatically, you can make sure there is no trash entries in MySQL by disabling the non existent plugins automatically, but one article won't be enough for that.

I hope it got your gray matter all buzzing and running, and now you'll create some awesome plugin systems for whatever project you're working on.

You're welcome to comment!!!

13 comments:

SwedishChef said...

Hey there,


I noticed this post was a wee bit old, but I had to give you some kudos. Very ncely done! I actually am implementing this in a project right now, and your code came in handy. Did you extend the functionality in any way later on?

Cheers

Unknown said...

I found this useful, but I am changing my setup to read from the database first, then load the plugins from file. This should help if there are lots of plugins that are not being used.

Andy said...

Thanks for posting this!

I was looking around for ideas on putting together a plugin type of system for my CMS project...

It was hard to find a tutorial/article on plugin theory (for PHP,) because I kept getting specific plugin-for-some-CMS tutorials in the search results... thanks.

Anonymous said...

Nice.. Next please show how this work with simple webpage.. like listening user and adding users. Very good if you can use some template system.

Tnx !

J said...

Thanks Danny for such a useful entry.

I however have a minor suggestion for anyone who wants to use this as their plug-in class, with multiple checkpoints that may or not be used, as the original method was throwing a lot of warnings to me when a plugin is not hooked on a checkpoint because it's not designed to.

Here's my modifications:

static function hook($checkpoint){
foreach(pluginClass::$plugins as $plugin) {
$callable = array($plugin, $checkpoint);
if (is_callable($callable)) {
call_user_func(array($plugin, $checkpoint));
}
}
}
//throw new Exception( "Cannot hook plugin ($plugin) at checkpoint ($checkpoint)" );
}*/
}
}

Hope it helps someone out there. :)

Anonymous said...

@J,
Thanks a lot for your modification to the script. Thats exactly what i wanted. I already read all the comments before running the script. When i wrote a test plugin and didn't write a method for the onLoad hook It threw a warning and an Exception and thats where your modification came to help :)
Anyway, thanks a lot Danny for such a wonderful tutorial.
Thumbs Up.

Unknown said...

Great, very clear. thanks for share!

phoenix2ch said...

Awesome tutorial, exactly what I was looking, thank you very much ! :)

Anonymous said...

Tank you for this very Amazone Tutorial. Of Course this is a Little Bit Old, but i have searched for a very Long Time bevore found this Article Here.

Gereetings from Germany

Anonymous said...

Some great tips! It will definitely help to me.

Website Development company

Unknown said...

Great php tutorials tips for programming. It can help better for php developers.Thanks for this useful post.

Ashwink said...

Thanks for sharing This valuable information.Also See My Academy Website Best Dot Net Training Institute in Chennai

Commodity Trading Tips said...

waoo nice post about "Do It Yourself - Plugins (PHP Tutorial)"

Thanks,

Silver Tips Free Trial