5

I am working on a script, and I need to make it pluginable. Now the syntax I have come with and which should work for me, is to make it use classes. For example, in order to create a new plugin that would be run when a certain point (hook) is reached, you would declare a new class. What I am not sure is how would the hook be specified in that syntax, so I am looking for suggestions.

Syntax example:

<?php
class ScriptPlugin
{
    function runPlugin() {} // would be run when the time has to come to execute this plugin
}
?>

Also, if that syntax is not going to work, it would be great if you guys could give me a good syntax example.

hakre
  • 193,403
  • 52
  • 435
  • 836
  • You have not added code to your question which actually would show the syntax you ask about. Please add that code otherwise it's hard to say. – hakre Dec 26 '11 at 15:34
  • possible duplicate of [Best way to allow plugins for a PHP application](http://stackoverflow.com/questions/42/best-way-to-allow-plugins-for-a-php-application) – hakre Apr 22 '13 at 07:36

3 Answers3

3

There is the Observer Pattern which comes to mind. Plugins will register themselves and will get notifications when the hook is invoked.

Another thing that comes to mind are callbacks in PHP. And there was a similar question already with an answer that came to mind. It shows hooks based on callbacks.

The Observer Pattern runs a bit short because with hooks you often want to provide things like arguments and a return value. The linked answer which uses callbacks does not have this either, so I wrote a little Hooks example class that provides named hooks/events to registered callbacks, and a way to register your own classes, e.g. a plugin.

The idea is pretty basic:

  • A hook has a name and zero or more callbacks attached.
  • All hooks are managed in a Hooks class.
  • The main code invokes hooks by calling a function on the Hooks object.
  • Plugins (and other classes) can register their own callbacks, which is done with the help of the Registerable interface.

Some example code with one plugin and two hooks:

<?php
Namespace Addon;

class Hooks
{
    private $hooks = array();
    private $arguments;
    private $name;
    private $return;
    public function __call($name, array $arguments)
    {
        $name = (string) $name;
        $this->name = $name;
        $this->arguments = $arguments;
        $this->return  = NULL;
        foreach($this->getHooks($name) as $hook)
        {
            $this->return = call_user_func($hook, $this);
        }
        return $this->return;
    }
    public function getHooks($name)
    {
        return isset($this->hooks[$name]) ? $this->hooks[$name] : array();
    }
    public function getArguments()
    {
        return $this->arguments;
    }
    public function getName()
    {
        return $this->name;
    }
    public function getReturn()
    {
        return $this->return;
    }
    public function setReturn($return)
    {
        $this->return = $return;
    }
    public function attach($name, $callback)
    {
        $this->hooks[(string) $name][] = $callback;
    }
    public function register(Registerable $plugin)
    {
        $plugin->register($this);
    }
}

interface Registerable
{
    public function register(Hooks $hooks);
}

class MyPlugin implements Registerable
{
    public function register(Hooks $hooks)
    {
        $hooks->attach('postPublished', array($this, 'postPublished'));
        $hooks->attach('postDisplayFilter', array($this, 'filterToUpper'));
    }
    public function postPublished()
    {
        echo "MyPlugin: postPublished.\n";
    }
    public function filterToUpper(Hooks $context)
    {
        list($post) = $context->getArguments();
        return strtoupper($post);
    }
}

$hooks = new Hooks();

$plugin = new MyPlugin();
$hooks->register($plugin);  

$hooks->postPublished();

echo $hooks->postDisplayFilter("Some post text\n");

I've done it this way to prevent that each Plugin must have a concrete base class only because it wants to make use of hooks. Additionally everything can register hooks, the only thing needed is a callback. For example an anonymous function:

$hooks->attach('hookName', function() {echo "Hook was called\n";});

You can however create yourself a plugin base class, that for example implements the register function and will automatically register functions that have a certain docblock tag or the name of a function

class MyNewPlugin extends PluginSuper
{
    /**
     * @hook postPublished
     */
    public function justAnotherFunction() {}

    public hookPostPublished() {}
}

The superclass can make use of Reflection to add the hooks on runtime. However reflection can slow things down and might make things harder to debug.

Community
  • 1
  • 1
hakre
  • 193,403
  • 52
  • 435
  • 836
  • This works just the way I wanted it to work. I tried to implement the class that would automatically register a function, that should be run when the plugin is initiated, but did not succeed. Unfortunately, I don't know much about OOP :( –  Dec 28 '11 at 21:37
  • The plugin needs to register it's function on a hook. You might want to add your code to at the end of my answer so I can take a look. – hakre Dec 28 '11 at 22:31
0

I would make a base abstract class with functions for all the hooks that could possibly be called.

abstract class Plugin {
    abstract function yourHook();
}

All plugin classes should inherit this base class, and will override those base functions with their own.

class SomePlugin extends Plugin {
    function yourHook() {
        echo 'yourHook() Called!';
    }
}

Now when your program runs, you need to find all of those plugin files to include, and somehow put them into an array, such as $plugins. See this article: https://stackoverflow.com/a/599694/362536

foreach (glob("classes/*.php") as $filename)
{
    include $filename;
}

(From Karsten)

Define a function accessible from everything, such as registerPlugin():

function registerPlugin($classname) {
    $plugins[] = new $classname();
}

Make the top line of each plugin file like this (prior to the class):

registerPlugin('SomePlugin');

If you do this, you'll have an array in $plugins with instances of each plugin. At the appropriate time, you can do something like this:

foreach ($plugins as $plugin) {
    $plugin->yourHook();
}

As an alternative, it may be more appropriate to use interfaces in your case, instead. You should decide which method is best for your application.

Community
  • 1
  • 1
Brad
  • 159,648
  • 54
  • 349
  • 530
  • All that is fine. It's exactly what I want to achieve. But, what I failed at, is that I could not recognize what is the name of the class that is in the plugin's file? I don't think it would be that good to require the plugin file to have the same name as the class, in order to know what name the class actually has, right? –  Dec 26 '11 at 15:46
  • I was thinking of a similar, but no the same thing though. This is good enough anyway, but I would first try to achieve what I am trying to do. One more shot is not going to cost my anything, and in case I am not satisfied, I will use this method. I'd like to thank you! –  Dec 26 '11 at 15:54
  • 1
    @user726049, look at `__autoload()`: http://php.net/manual/en/language.oop5.autoload.php – Minras Dec 26 '11 at 15:56
0

Let's say a plugin is like :

class NewsPlugin extends Plugin
{
  function onCreate($title)
  {
    # Do some stuff
  }
}

Then when you create a news you can just call onCreate on all plugins registered.

Cydonia7
  • 3,744
  • 2
  • 23
  • 32
  • That's the syntax I am trying to achieve. But, I experience problem in finding all the plugins. How could I do that? –  Dec 26 '11 at 15:40
  • I usually put them all in a directory called `plugins` and require them all in my header using `glob`. They all extend the same base class. – Cydonia7 Dec 26 '11 at 15:42
  • What do you think if I could the a __construct() method in the Plugin class, so when a new instance is created and based on that class, it should somehow proceed the $this variable to the plugins array? –  Dec 26 '11 at 15:43
  • Of course this works but $plugins should be a global variable and plugins still need to be constructed. – Cydonia7 Dec 26 '11 at 15:44
  • I see. Also, even if I added __construct() in the class Plugin, again I still would not be able to run a global plugin functions, lets say "runPlugin()" or so, would I? –  Dec 26 '11 at 15:47
  • You can create a function runPlugins($plugins, $action) mapping over the $plugins array and executing the hook. – Cydonia7 Dec 26 '11 at 15:49
  • I don't think to understand what you are trying to say with that? –  Dec 26 '11 at 15:51
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/6144/discussion-between-skydreamer-and-user726049) – Cydonia7 Dec 26 '11 at 15:53