You know almost every time I create a plugin API for a project, I do it differently. A text book observer/obervable works well for cases where you have an object that you want to make plugins for, but not so well for cases where you may have many objects you want accessible to plugins or even entire applications [at least not the textbook sort of case].
The one time for a generic service plugin API that covered an entire application, I had the service plugin, which loaded and called the plugins, initialized it and placed it in a registry, and then just placed virtual calls to it wherever I felt they were needed or would be useful. I used __call() magic, so the calls were in the form $service_plugins->doSomeAction($param-1, … $param-n);
d11wtq, I was intrigued by your first example which gives the plugin a base object it can work with. So expanding upon that, and the idea of point cuts, I just wanted to see what I could come up with this time around, so just whipped this up. Mind you I haven’t had much time to think about it, and this approach is bound to be flawed in several ways, but it was a fun exercise.
Here is the real heart of it, first the Plugins base class, made abstract to allow more than one “plugin group” in the app:
abstract class Plugins {
protected $plugins = array();
public function __construct($plugins) {
foreach ($plugins as $plugin) {
$plug = new $plugin();
if ($plug instanceof Plugin) {
$this->plugins[] = $plug;
}
}
}
public function __call($method, $args) {
$baseObject = false;
if ((sizeof($args) > 0) && is_object($args[0])) {
$baseObject = array_shift($args);
}
foreach ($this->plugins as $key => $plugin) {
if (!method_exists($plugin, $method)) {
continue;
}
if ($baseObject !== false) {
$this->plugins[$key]->loadBaseObject($baseObject);
} else {
$this->plugins[$key]->loadBaseObject(NULL);
}
call_user_func_array(array(&$this->plugins[$key], $method), $args);
}
}
}
The the individual Plugin core:
abstract class Plugin {
protected $baseInstance = NULL;
public function loadBaseObject($object = NULL) {
$this->baseInstance = $object;
}
public function __call($method, $args) {
if ($this->baseInstance === NULL) {
return false;
}
if (preg_match('/^do(\\w+)$/', $method, $matches)) {
$method = strtolower($matches[1]{0}) . substr($matches[1], 1);
$return = NULL;
$plugin_method = ucfirst($method);
$plugin_args = $args;
array_unshift($plugin_args, $this->baseInstance);
call_user_func_array(array(&$this->baseInstance->plugins, 'before' . $plugin_method), $plugin_args);
if (method_exists($this, $method)) {
$return = call_user_func_array(array(&$this, $method), $args);
}
call_user_func_array(array(&$this->baseInstance->plugins, 'on' . $plugin_method), $plugin_args);
call_user_func_array(array(&$this->baseInstance->plugins, 'after' . $plugin_method), $plugin_args);
return $return;
}
}
}
And then the core for “Plugabble” objects:
abstract class Pluggable {
public $plugins = array();
public function __construct($plugins) {
$this->plugins = $plugins;
}
public function __call($method, $args) {
if (preg_match('/^do(\\w+)$/', $method, $matches)) {
$method = strtolower($matches[1]{0}) . substr($matches[1], 1);
$return = NULL;
$plugin_method = ucfirst($method);
$plugin_args = $args;
array_unshift($plugin_args, $this);
call_user_func_array(array(&$this->plugins, 'before' . $plugin_method), $plugin_args);
if (method_exists($this, $method)) {
$return = call_user_func_array(array(&$this, $method), $args);
}
call_user_func_array(array(&$this->plugins, 'on' . $plugin_method), $plugin_args);
call_user_func_array(array(&$this->plugins, 'after' . $plugin_method), $plugin_args);
return $return;
}
}
}
The way this works, is that if you call doMethodName() on a Pluggable object, it will set itself as the current base object for the plugin group the object is using, call beforeMethodName() across the plugin group, call & get result of methodName() in the object [if it exists], call onMethodName() across the plugin group, call afterMethodName() across the plugin group, and finally return the result. And then basically the same is true of Plugins as well, so in addition to being responsive to Pluggable objects, Plugins can also be responsive to each other as well. And the Pluggable’s method doesn’t have to exist, so you can make virtual event/action calls.
And then here is a quite simple & silly example I did of how one could implement the above API.
First a “plugin group”. I’m only going to have one group in this example, but naturally one could have as many as they wished. I also don’t have anything in this group such as specific methods for the group on whole, but you naturally can and a more complex example probably would:
class SpeakerPlugins extends Plugins { }
Then a simple ClassRoom that is Plugabble:
class ClassRoom extends Pluggable {
public $noise = false;
}
Then a Pluggable speaker:
class Speaker extends Pluggable {
public $speaker;
public $ended = false;
public function __construct($speaker, $plugins) {
parent::__construct($plugins);
$this->speaker = $speaker;
}
protected function speak($word) {
echo "{$this->speaker}: $word<br />";
}
public function speakNormally($word) {
$this->speak($word);
}
public function speakSoftly($word) {
$this->speak('<small>'.strtolower($word).'</small>');
}
public function speakLoudly($word) {
$this->speak(strtoupper($word));
}
public function endSpeech($text) {
if ($this->ended === true) {
return;
}
if ($text !== false) {
$this->doSpeakNormally($text);
}
$this->ended = true;
}
}
Then a Speech, which I didn’t make Pluggable:
class Speech {
public $speaker;
public function __construct(Pluggable $speaker) {
$this->speaker = $speaker;
}
public function giveSpeech($texts, $type = 'normal') {
$this->speaker->doStartSpeech();
if (!is_array($texts)) {
$texts = array($texts);
}
foreach ($texts as $text) {
switch ($type) {
case 'soft':
$this->speaker->doSpeakSoftly($text);
break;
case 'loud':
$this->speaker->doSpeakLoudly($text);
break;
case 'normal':
default:
$this->speaker->doSpeakNormally($text);
break;
}
}
$this->speaker->doEndSpeech('Thank You.');
}
}
Then an abstract SpeakerPlugin for the SpeakerPlugins group:
abstract class SpeakerPlugin extends Plugin {
public $speaker;
protected function speak($text) {
echo "{$this->speaker}: $text<br />";
}
}
Then a Bell SpeakerPlugin:
class Bell extends SpeakerPlugin {
public $speaker = 'Bell';
public function onBeginClass() {
$this->speak('<<<bell rings>>>');
$this->baseInstance->noise = false;
}
public function onEndClass() {
$this->speak('<<<bell rings>>>');
}
protected function speak($text) {
echo "<strong>$text</strong><br />";
}
}
Then a Teacher SpeakerPlugin:
class Teacher extends SpeakerPlugin {
public $speaker = 'Teacher';
public function afterBeginClass() {
if ($this->baseInstance->noise === true) {
$this->speak("Hush children! The bell has rung.");
$this->baseInstance->noise = false;
}
}
public function afterEndClass() {
if ($this->baseInstance->noise === true) {
$this->speak("May I have your attention for a moment please!");
$this->baseInstance->noise = false;
}
$this->speak("Your reports are due 1 week from today. Have a nice weekend");
}
public function onStartSpeeches() {
if ($this->baseInstance->noise === true) {
$this->speak("Shhh!");
$this->baseInstance->noise = false;
}
}
public function beforeStartSpeech() {
$this->speak("{$this->baseInstance->speaker} is now going to give a speech.");
}
public function afterSpeakSoftly($word) {
$this->doReprimandSpeaker("Speak Up {$this->baseInstance->speaker}!");
$this->baseInstance->doSpeakNormally($word);
}
public function afterSpeakLoudly($word) {
$this->doReprimandSpeaker("Thank you {$this->baseInstance->speaker}, but next time don't yell.");
$this->baseInstance->doEndSpeech(false);
}
protected function reprimandSpeaker($text) {
$this->speak($text);
}
}
And finally Students SpeakerPlugin:
class Students extends SpeakerPlugin {
public $speaker = 'ClassRoom';
protected $canApplaud = true;
public function beforeBeginClass() {
$this->speak("<<<giggling & loud talking>>>");
$this->baseInstance->noise = true;
}
public function afterBeginClass() {
$this->speak("<<<giggling & loud talking>>>");
$this->baseInstance->noise = true;
}
public function afterEndClass() {
$this->speak("<<<giggling & loud talking>>>");
$this->baseInstance->noise = true;
}
public function beforeStartSpeeches() {
$this->speak("<<<giggling>>>");
$this->baseInstance->noise = true;
}
public function onReprimandSpeaker() {
$this->speak("<<<giggles>>>");
}
public function beforeEndSpeech($text) {
if ($this->baseInstance->ended === true) {
$this->canApplaud = false;
} else {
$this->canApplaud = true;
}
}
public function afterEndSpeech($text) {
if (($text !== false) && $this->canApplaud) {
$this->speak("<<<applause>>>");
}
}
}
Whew.
And now putting it all together something like:
$load_plugins = array(
'Students',
'Teacher',
'Bell',
);
$plugins = new SpeakerPlugins($load_plugins);
$class = new ClassRoom($plugins);
$class->doBeginClass();
echo '<hr />';
$class->doStartSpeeches();
echo '<hr />';
$speech = new Speech(new Speaker('Bob', $plugins));
$speech->giveSpeech('Foo Bar Foo Foo.', 'soft');
echo '<hr />';
$speech = new Speech(new Speaker('Linda', $plugins));
$speech->giveSpeech(array(
'Foo Bar Foo Foo.',
'Foo Bar Foo Foo.',
'Foo Bar Foo Foo.',
'Foo Bar Foo Foo.',
));
echo '<hr />';
$speech = new Speech(new Speaker('John', $plugins));
$speech->giveSpeech('Foo Bar Foo Foo.', 'loud');
echo '<hr />';
$class->doEndSpeeches();
echo '<hr />';
$class->doEndClass();
Which outputs something like:
ClassRoom: <<<giggling & loud talking>>>
<<<bell rings>>>
ClassRoom: <<<giggling & loud talking>>>
Teacher: Hush children! The bell has rung.
ClassRoom: <<<giggling>>>
Teacher: Shhh!
Teacher: Bob is now going to give a speech.
Bob: foo bar foo foo.
Teacher: Speak Up Bob!
ClassRoom: <<<giggles>>>
Bob: Foo Bar Foo Foo.
Bob: Thank You.
ClassRoom: <<<applause>>>
Teacher: Linda is now going to give a speech.
Linda: Foo Bar Foo Foo.
Linda: Foo Bar Foo Foo.
Linda: Foo Bar Foo Foo.
Linda: Foo Bar Foo Foo.
Linda: Thank You.
ClassRoom: <<<applause>>>
Teacher: John is now going to give a speech.
John: FOO BAR FOO FOO.
Teacher: Thank you John, but next time don’t yell.
ClassRoom: <<<giggles>>>
<<<bell rings>>>
ClassRoom: <<<giggling & loud talking>>>
Teacher: May I have your attention for a moment please!
Teacher: Your reports are due 1 week from today. Have a nice weekend
If you play around with the example a bit, you’ll notice that in addition to being responsive to the Pluggable objects, particularly the Teacher & Students Plugins are quite responsive to each other too. For example, if the students aren’t being noisy, the Teacher doesn’t have to keep telling them to be quite.
I think this may be one of my favorite implementations to date, though I haven’t had a lot of time to think it over or test it in a real scenario, so I reserve the right to take that comment back 