Writing dynamic plugins for PHP Classes/OOP

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Writing dynamic plugins for PHP Classes/OOP

Post by Chris Corbyn »

I'm interested in hearing how you guys would go about creating classes that support plugins. I'm really keen to hear ways in which you can have plugins respond to events that are triggered inside the class they are plugged into and how you actually load and contain the plugin. But generally, I'm just looking for a healthy discussion on plugin development for PHP Classes :)

I've just yesterday done something like this:

Code: Select all

class baseClass
{
    private $plugins = array();

    public $word = '';
   
    public function loadPlugin(&$object)
    {
        $plugin_name = $object->identifier;
        $object->loadBaseObject(&$this);
        $this->plugins[$plugin_name] =& $object;
    }

    //This sets of logic in the plugin
    private function triggerEvents($event)
    {
        foreach ($this->plugins as $name => $object)
        {
            if (method_exists($object, $event))
            {
                $this->plugins[$name]->$event();
            }
        }
    }

    private function speak($word)
    {
        $this->word = $word;
        $this->triggerEvents('onSpeak');
        echo $this->word;
    }

    public function &getPlugin($name)
    {
        return $this->plugins[$name];
    }
}

class dummyPlugin
{
    private $baseInstance;
    
    public $identifier = 'dummy'; //The name of the plugin
    
    public function loadBaseObject(&$object)
    {
        $this->baseInstance =& $object;
    }
    
    public function test()
    {
        echo 'Test called...';
    }

    //Only runs when baseClass allows it to
    public function onSpeak()
    {
        $this->baseInstance->word .= '... touched by a plugin';
    }
}

$base = new baseClass;
$base->speak('foo'); //Foo

$base->loadPlugin(new dummyPlugin);

$base->speak('bar'); //bar ... touched by a plugin

$base->getPlugin('dummy')->test(); // Test called
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

I've just had an interesting read of something to do with AspectPHP.

http://www.sebastian-bergmann.de/Aspect ... tcuts.html

It looks as though some of the ideas I used above are vaguely like I was trying to create pointcuts in the code even though I didn't know that's what I was doing :P

I was thinking about how you'd go about intersecting points in the logic and method calls etc but PHP is not really suited for that sort of coding I guess (hence the lack of response to this thread?).
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I have a couple of thoughts. I think I mentioned awhile back about not liking the loadBaseObject() fuctionality. It is usual for the plugin to be passed any data it needs when the callback occurs -- often the calling object itself (i.e. $this). I think that simplfies things.

Regarding the interface it seems like there are three ways to go:

1. a single plugin that is passed which point/state from which it is being called
2. a single plugin with a method per plugin point
3. an individual plugin for each plugin point

I think usage determines which one to use.
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

arborint wrote:I have a couple of thoughts. I think I mentioned awhile back about not liking the loadBaseObject() fuctionality. It is usual for the plugin to be passed any data it needs when the callback occurs -- often the calling object itself (i.e. $this). I think that simplfies things.

Regarding the interface it seems like there are three ways to go:

1. a single plugin that is passed which point/state from which it is being called
2. a single plugin with a method per plugin point
3. an individual plugin for each plugin point

I think usage determines which one to use.
Ah you're quite right about loadBaseObject() being a bit off-ish. In fact I was just think the same earlier but I wrote a little interface to make things more obvious all the same, it's still a bit whacky... passing the object to be used locally would avoid that "recursive object strcuture" and make the logic a little more readable perhaps.

Little did I know that I'm basically trying to go beyond OOP and work with AOP in PHP (which of course isn't an AOP language).

The thing with me is I've never written anything that's designed to be pluggable before so this is all new to me and I'm sort of exploring if you like :)

Your points:

1. It's clean but it doesn't really allow you to do very much with a single plugin - then again, that's probably a good thing - a plugin should probably do one specific task and that's it. How might you code this if it needs to run multiple times without user-interaction though?
2. This would (perhaps) solve an issue I was thinking regarding "clashes" with multiple plugins... it still feels a little restrictive though.
3. Same sort of thing as above but better factored.

I think I've gotten into the wrong frame of mind from the outset. JavaScript was on my mind, hence my consideration for event handlers.

OK, now sit down:

"Forgive me for I have sinned dear lord...." :P I've posted on SitePoint :oops:

One guy posted this however which also realtes with my way of of thinking about Javascript events (it's hacky like my stuff too though - just a lot more lengthy).
Ezku on SitePoint wrote:Here's something experimental I threw together a year ago. Oh, how this makes me wish PHP had closures...

Code: Select all

/**
 * Delegate interface. Describes a kind of refined callback.
 * 
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		Sep 6, 2005
 */
interface IDelegate
{
	/**
	 * Invoke Delegate
	 * @param	array	arguments, optional
	 */
	public function invoke($args = array());
}

/**
 * Calls a method on an object upon invocation.
 * 
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		Sep 6, 2005
 */
class Delegate implements IDelegate
{
	private $subordinate = NULL;
	private $method = NULL;
	
	public function __construct($subordinate, $method)
	{
		$this->subordinate = $subordinate;
		$this->method = $method;
	}
	
	public function invoke($args = array())
	{
		$this->subordinate = Handle::resolve($this->subordinate);
		$callback = array(&$this->subordinate, $this->method);
		return call_user_func_array($callback, $args);
	}
}

Code: Select all

/**
 * "A Handle represents an uninstantiated object that takes the place of a
 * given object and can be swapped out for the object.
 * Implements lazy loading for composing object heirarchies."
 * 
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		Jul 12, 2005
 * @see			[url]http://wact.sourceforge.net/index.php/ResolveHandle[/url]
 */
class Handle
{
	/**
	 * @var	string	class name
	 */
	protected $class = NULL;
	/**
	 * @var	array	class constructor arguments
	 */
	protected $args = array();
	
	/**
	 * @param	string	class name
	 * @param	array	class constructor arguments, optional
	 */
	public function __construct($class, $args = array())
	{
		$this->class	= (string)	$class;
		$this->args		= (array)	$args;
	}
	
	/**
	 * Resolves a Handle; replaces a Handle instance with its identified class
	 * @param	object	Handle
	 * @return	object
	 */
	static public function resolve($handle)
	{
		if ($handle instanceof self)
		{
			$handle = call_user_constructor_array($handle->getClass(), $handle->getArgs());
		}
		return $handle;
	}
	
	public function getClass() { return $this->class; }
	public function getArgs() { return $this->args; }
}

Code: Select all

/**
 * Simple Event: attach a Delegate and trigger.
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		1.10.2005
 */
class Event
{
	private $listener = NULL;
	
	/**
	 * @param	object	IDelegate
	 * @return	boolean	overridden
	 */
	public function attach(IDelegate $listener)
	{
		$overridden = isset($this->listener);
		$this->listener = $listener;
		return $overridden;
	}
	
	/**
	 * @param	array	invocation arguments
	 * @return	mixed	results
	 */
	public function trigger($args = array())
	{
		return $this->listener->invoke($args);
	}
	
	/**
	 * Get attached listener
	 * @return	object	IDelegate
	 */
	public function getAttached()
	{
		return $this->listener;
	}
}
/**
 * Multicast Event: attach multiple Delegates.
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		1.10.2005
 */
class MulticastEvent extends Event
{
	private $listeners = array();
	
	/**
	 * @param	object	IDelegate
	 */
	public function attach(IDelegate $delegate)
	{
		$this->listeners[] = $delegate;
	}
	
	/**
	 * @param	array	invocation arguments
	 * @return	array	results
	 */
	public function trigger($args = array())
	{
		$return = array();
		foreach($this->listeners as $listener)
		{
			$return[] = $listener->invoke($args);
		}
		return $return;
	}
	
	/**
	 * Get attached listeners
	 * @return	array	IDelegate
	 */
	public function getAttached()
	{
		return $this->listeners;
	}
}

Code: Select all

/**
 * Handle groups of events
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		26.9.2005
 */
class EventHandler
{
	private $args = array();
	private $events = array();
	
	/**
	 * EventHandler consturctor
	 * @param	mixed	default invocation argument
	 * ...
	 */
	public function __construct()
	{
		$this->args = func_get_args();
	}
	
	/**
	 * Attach a listener to an event
	 * @param	string	event name
	 * @param	object	IDelegate
	 */
	public function attach($event, IDelegate $listener)
	{
		if(empty($this->events[$event]))
		{
			$this->events[$event] = $this->getEvent();
		}
		$this->events[$event]->attach($listener);
	}
	
	/**
	 * Trigger an event
	 * @param	string	event name
	 * @param	array	arguments, optional
	 */
	public function trigger($event, $args = array())
	{
		if(empty($args))
		{
			$args = $this->getArgs();
		}
		$return = NULL;
		if(!empty($this->events[$event]))
		{
			$return = $this->events[$event]->trigger((array) $args);
		}
		return $return;
	}
	
	/**
	 * @return	array	default invocation arguments
	 */
	protected function getArgs()
	{
		return $this->args;
	}
	
	/**
	 * @return	object	a fresh Event
	 */
	protected function getEvent()
	{
		return new Event;
	}
	
	/**
	 * Shortuct for trigger()
	 */
	public function __call($event, $args)
	{
		return call_user_func_array(array($this, 'trigger'), array($event, $args));
	}
	
	/**
	 * Shortcut for attach()
	 */
	public function __set($event, $args)
	{
		return call_user_func_array(array($this, 'attach'), array($event, $args));
	}
}
/**
 * Extend EventHandler to use MulticastEvents
 * 
 * @author		Ezku (dmnEe0@gmail.com)
 * @since		26.9.2005
 */
class MulticastEventHandler extends EventHandler
{
	protected function getEvent()
	{
		return new MulticastEvent;
	}
}
It's supposed to be used like this (a total nonsense example, bear with me):

Code: Select all

$events = new EventHandler;
$events->onload = new Delegate(new Handle('Controller'), 'execute');
$events->onload(Context::getInstance); // lazy-loads and creates a Controller instance, calls execute() with the arguments given
Here's a slightly more elaborate example, albeit on an older version of the code.

The whole idea is hacky to begin with, but aspect oriented programming in PHP seems doubly so to me. Just gives me a bad vibe. :)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:Little did I know that I'm basically trying to go beyond OOP and work with AOP in PHP (which of course isn't an AOP language).

The thing with me is I've never written anything that's designed to be pluggable before so this is all new to me and I'm sort of exploring if you like :)
I think you have gone a little astray with AOP -- you just need plugins.
d11wtq wrote:Your points:

1. It's clean but it doesn't really allow you to do very much with a single plugin - then again, that's probably a good thing - a plugin should probably do one specific task and that's it. How might you code this if it needs to run multiple times without user-interaction though?
2. This would (perhaps) solve an issue I was thinking regarding "clashes" with multiple plugins... it still feels a little restrictive though.
3. Same sort of thing as above but better factored.
None of the points I posted have any less capabilities, they only have different interfaces. You can implement anything in anyone of them. They are just more or less convenient depending on the useage. The main decision points are: one or many plugins, and if one then state param or call method. I think supporting chains of plugins is a little overboard given that it will probably never be used. But a simple array based chain would be enough.

I think you have jumped the shark a little with your last release though. ;)

PS - Ezku is a good guy, but he is right -- that is old code from all over the place (WACT, Lastcraft, Skeleton, etc.).
(#10850)
santosj
Forum Contributor
Posts: 157
Joined: Sat Apr 29, 2006 7:06 pm

Post by santosj »

Well, what do you know, I was thinking of implementing a similar idea with a little different method. Using SPL Observer classes, but I suppose I could just rip your code instead. However I feel that if you are using PHP 5, then you should use the Reflection Classes.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

santosj wrote:Well, what do you know, I was thinking of implementing a similar idea with a little different method. Using SPL Observer classes, but I suppose I could just rip your code instead. However I feel that if you are using PHP 5, then you should use the Reflection Classes.
I'm not really familiar with the reflection classes, although I've seen a little example code with them... I'll have a look thanks :) If the code changed enough to depend on these classes it would mean that my PHP4 conversion, which at present is syntactical only, would go out of the window.
santosj
Forum Contributor
Posts: 157
Joined: Sat Apr 29, 2006 7:06 pm

Post by santosj »

d11wtq wrote:I'm not really familiar with the reflection classes, although I've seen a little example code with them... I'll have a look thanks :) If the code changed enough to depend on these classes it would mean that my PHP4 conversion, which at present is syntactical only, would go out of the window.
This is true, but PHP 5 is the wave of the future, so forget about the old man PHP 4. :)

I've found that the Reflection classes are pretty straight forward and easy to work with. It wouldn't be hard to create your own code without going to tutorial sites.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I am not sure that Reflection is the way to go with this. Plugins needs a explicit interface that plugin builders can follow -- and predictable behavior. Using reflection would add some intellegence where none is really needed, and hurt performance in the process.
(#10850)
santosj
Forum Contributor
Posts: 157
Joined: Sat Apr 29, 2006 7:06 pm

Post by santosj »

arborint wrote:I am not sure that Reflection is the way to go with this. Plugins needs a explicit interface that plugin builders can follow -- and predictable behavior. Using reflection would add some intellegence where none is really needed, and hurt performance in the process.
D11wtq example could be completely replaced by the SPL Observer Classes without the need for Reflection. Since SPL is compiled into PHP, it would be faster than an user defined class. The example also implement some of the Reflection Class functionality. By using both SPL Observer and Reflection, the classes would be smaller and faster.

How is this:

Code: Select all

$callback = array(&$this->subordinate, $this->method);
return call_user_func_array($callback, $args);
A performance hit over

Code: Select all

$method = new ReflectionMethod($this->method);
return $method->invokeArgs($this->subordinate, $args);
Not only is it easier, but the cause of error is reduced. I have not done any benchmarks... as I haven't been able to get call_user_func_array to work correctly and decided using Reflection was less stressful.

Disclaimer: I meant no disrespect to D11wtq, only trying to prove me point and I'm sorry in advance.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

santosj wrote:Disclaimer: I meant no disrespect to D11wtq, only trying to prove me point and I'm sorry in advance.
Believe me, I don't take offense at criticism ;) If something I've done can be done better or just doesn't fell right I'd like to know :) (I might not choose to accept it but either way I wonuld never be offended).

Re PHP5 being the way of the future... As much as I prefer PHP5 over 4 I don't feel it is. PHP6 is already in development, PHP5 never really got off the ground as much as it should have done due to shared hosts failing to bother upgrading and PHP4 is still the most highly used. I do believe that by the time PHP6 is out more people will use it than PHP4 because the changes they've added ake it more worthwhile upgrading - that's another topic all together though.

/heads off for more background reading
santosj
Forum Contributor
Posts: 157
Joined: Sat Apr 29, 2006 7:06 pm

Post by santosj »

Do not worry about copying because in PHP 5, classes are passed by reference.


First Example:

Code: Select all

class Word implements SplSubject
{
	private $observers = array();
	
	private $word = '';
	
	function attach(SplObserver $plugin)
	{
		$class = get_class($plugin);
		$this->observers[$class] = $plugin;
	}
	
	function detach(SplObserver $plugin)
	{
		$class = get_class($plugin);
		unset($this->observers[$class]);
	}
	
	function notify()
	{
		foreach($this->observers as $plugin)
		{
			$plugin->update($this);
		}
	}
	
	function speak($word)
	{
		$this->word = $word;
	}
	
	public function get()
	{
		return $this->word;
	}
	
	function __construct()
	{
		
	}
	
	function __toString()
	{
		return $this->word ."<br />\n";
	}
}

class Plugin implements SplObserver
{
	function update(SplSubject $subject)
	{
		$word = $subject->get() . '... touched by a plugin';
		$subject->speak($word);
	}
}

$word = new Word;
$plugin = new Plugin;

$word->speak('testing');
$word->attach($plugin);

Code: Select all

echo $word;

Code: Select all

testing

Code: Select all

$word->notify();

echo $word;

Code: Select all

testing... touched by a plugin
This gives each plugin the builtin interface for which to implement allowing the plugin to only worry about updating the subject. With this, you could perhaps build a paragraph based entirely on which plugins were attached and in which order they were attached.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Haha sweet cheers! :)
santosj
Forum Contributor
Posts: 157
Joined: Sat Apr 29, 2006 7:06 pm

Post by santosj »

Yeah, thank http://wiki.cc/php/SplObserver and http://wiki.cc/php/SPL

I still haven't converted the other example over, but I'll do that later. There are flaws in the design of my example that would need to be worked out. I was going to give the plugin an public variable that would the key for deletion, but I never really got around to it.
Post Reply