Extending a PHP class with plugins

It’s has been a while since I post anything useful or technical. My last posts all have ben sponsored by some one!

When I was writing the new version of phMagick I had a few requirements in mind

It should be:

  • Compatible with previous versions
  • Easy to incorporate patches and new features the community sends back
  • Small footprint (let the user choose what features he needs)

Having one big class would defeat the last 2 requirements, it would be too costly to manage contributions, and obviously there is no way to include just the features needed!

I tried some workarounds to php lack of multi inheritance support, but the code was just becoming to messy!

I come up with a solutions to “extend” a class by using a plugin system.

This way it is easy to include only the features you need and managing contributions is easy also!

The trick is relying on some magic functions, I’m not a support of magic in the code as I think it makes it more error prone and harder to debug and test, but in this case there is really no magic in those functions as they are all documented.

A closer look to PHP method overloading documentation pointed me to the __call()  and call_user_func_array() functions.

When the class is instantiated the constructor loads all the plugins found and stores them in a array.

When a method not present in the class is called PHP automatically runs the __call() that’s where I can see if the called function is available in any plugin, if so I just execute it.

 

The master class code

class Master
{
    //store available plugin classes
    private $plugins = array();
 
    //store methods found on each plugin
    private $methods = array();
 
    function __construct()
    {
        $this->loadPlugins();
    }
 
    /**
     * 
     * this functio will load all available plugin into memory
     * Rules:
     * 		plugin is store nin ./plugins
     * 		plugin class is named plugin_>:bla>
     * 		php file is named >bla>.php
     */
    private function loadPlugins()
    {
        $base = realpath(dirname(__FILE__)) . "/plugins&";
        $plugins = glob($base . "/*.php";);
        foreach($plugins as $plugin){
            include_once $plugin ;
            $name = basename($plugin, ".php";);
            $className = "plugin_".$name ;
            //create the plugin object so it can be stores and called later
            $obj = new $className();
            $this->plugins[$name] = $obj ;
 
            //store all methods found in the plugin
            foreach (get_class_methods($obj) as $method )
                 $this->methods[$method] = $name ;
        }
    }
 
    /**
     * 
     * Run the plugin method
     * @param strint $method
     * @param string $args
     * 
     */
    public function __call($method, $args){
        if(! key_exists($method, $this->methods))
           throw new Exception ("Call to undefined method : " . $method);
 
           array_unshift($args, $this);
           return call_user_func_array(array($this->plugins[$this->methods[$method]], $method), $args);
    }
}

The plugin class code, save it as ./plugins/hello.php

class plugin_hello
{
    function hello_world()
    {
        echo "hello";
    }
}

 

How to run it

$m = new Master();
$m->hello_world();

 

Do you know other way to accomplish the same?

Share your thoughts, I'm looking forward.

 

After the codes, why not try reading up on some php hosting providers? They should come in handy when the situation calls for it.

3 thoughts on “Extending a PHP class with plugins

  1. Bom post, não conhecia esse método. Já conhecia as funções __get e __set(), mas tinha lido que não deviam ser usadas pois afectavam negativamente a performance da aplicação. Gostava de saber se com a função __call() acontece o mesmo.

    De qualquer maneira, bom post. :)

  2. @Scorch

    Sim, como em todas as magical functions vamos ser sempre afetados pela pior performance.

    Para mim é muito relativo usar ou não um procedimento baseado em performance, se vai ser executado uma vez não é um problema grande, agora se vai ser executado 10 000 vezes ai já se sente a pior performance.

  3. Olá Nuno,

    Primeiro, esta linha está mal:
    $base = realpath(dirname(__FILE__)) . “/plugins&”;

    Devia ser (sim só muda o fim ehehe):
    $base = realpath(dirname(__FILE__)) . “/plugins/”;

    Além disso, já que estás numa de fazer um loader…

    class Loader{
    private $MASTER = NULL;

    function __construct(){
    $this->MASTER = Master::getInstance();
    }

    public function plugin( $plugin){
    $base = realpath(dirname(__FILE__)) . “/plugins/”;
    include_once “$base/” . basename($plugin) . “.php” ;
    $name = basename($plugin, “.php”;);
    $className = “plugin_”.$name ;
    //create the plugin object so it can be stores and called later
    if(!isset($this->MASTER->$name))
    $this->MASTER->$name = new $className();

    return $this;
    }
    }

    class Master{
    protected static $instance = NULL;

    function __construct(){
    self::$instance = $this;
    $this->load = new Loader();
    }

    public static function getInstance(){
    return self::$instance;
    }
    }

    /* No ficheiro plugins/hello.php */
    class plugin_hello
    {
    function hello_world()
    {
    echo “hello”;
    }
    }

    /** NO SCRIPT **/
    $teste = new Master();
    $teste->load->plugin( ‘hello’ );
    $teste->hello->hello_word;

    # Assim acho um melhor uso…
    # Podias também definir o método directo…
    # mas convém validares se já tens 2 plugins com o mesmo método
    # da mesma forma que valido no meu código se já existe 2
    # ‘propriedades’ com o mesmo nome.
    #
    # BTW, retorno $this no método load() porque se quiseres
    # fazer algo como: $this->load->plugin( “plugin” )->( “plugin2″ )
    # automatiza o processo de load :) .
    #
    #
    #
    # Abraço

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>