Your model could easily broadcast more particular events, such as onWeightChange or even onWeightGain. If you have worked with javascript/DOM, you will recognize that the DOM is following this pattern. In javascript, you can subscribe to events that happen in the DOM (Document Object Model).
A couple of weeks ago, I was tinkering with a small experiment. The code for subscribe/notify is something that you need in a lot of places, so I have had trouble finding out where to put it. Currently, my model classes will either share a common base class, or I'll have to reimplement the same few lines of code in multiple places - not exactly the best solution.
So inspired by javascript's event model, I stitched the following together:
PHP Code:
Signals::$instance = new Signals();
function connect($subject, $event, $objOrFnc, $methodOrNothing = NULL) {
return Signals::$instance->connect($subject, $event, $objOrFnc, $methodOrNothing);
}
function signal($subject, $event) {
$args = func_get_args();
array_shift($args);
array_shift($args);
return Signals::$instance->signalArgs($subject, $event, $args);
}
function signalArgs($subject, $event, $args) {
return Signals::$instance->signalArgs($subject, $event, $args);
}
function signalAll($subject) {
return Signals::$instance->signalAll($subject);
}
function disconnect($subject, $event, $objOrFnc, $methodOrNothing = NULL) {
return Signals::$instance->disconnect($subject, $event, $objOrFnc, $methodOrNothing);
}
function disconnectAll($subject, $event) {
return Signals::$instance->disconnectAll($subject, $event);
}
function hasConnected($subject, $event) {
return Signals::$instance->hasConnected($subject, $event);
}
class Signals_BreakSignalException extends Exception {}
/**
* Class is a singleton. You probably don't want to instantiate it, but
* rather use the globally scoped functions.
*/
class Signals
{
static $instance;
protected $subjects = Array();
protected $delegates = Array();
protected function delegate($objOrFnc, $methodOrNothing = NULL) {
foreach ($this->delegates as $key => $delegate) {
if ($delegate[0] === $objOrFnc && $delegate[1] === $methodOrNothing) {
return $key;
}
}
$this->delegates[] = Array($objOrFnc, $methodOrNothing);
return count($this->delegates) - 1;
}
public function connect($subject, $event, $objOrFnc, $methodOrNothing = NULL) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
if (!isset($this->subjects[$d])) {
$this->subjects[$d] = Array();
}
if (!isset($this->subjects[$d][$event])) {
$this->subjects[$d][$event] = Array();
}
$this->subjects[$d][$event][] = $this->delegate($objOrFnc, $methodOrNothing);
}
public function disconnect($subject, $event, $objOrFnc, $methodOrNothing = NULL) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
if (!isset($this->subjects[$d])) {
return;
}
if (!isset($this->subjects[$d][$event])) {
return;
}
$d2 = $this->delegate($objOrFnc, $methodOrNothing);
$tmp = Array();
foreach ($this->subjects[$d][$event] as $id) {
if ($id != $d2) {
$tmp[] = $id;
}
}
$this->subjects[$d][$event] = $tmp;
}
public function disconnectAll($subject, $event) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
if (!isset($this->subjects[$d])) {
return;
}
if (!isset($this->subjects[$d][$event])) {
return;
}
$this->subjects[$d][$event] = Array();
}
public function signal($subject, $event) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$args = func_get_args();
array_shift($args);
array_shift($args);
return $this->signalArgs($subject, $event, $args);
}
public function signalArgs($subject, $event, $args) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
if (isset($this->subjects[$d]) && isset($this->subjects[$d][$event])) {
foreach ($this->subjects[$d][$event] as $id) {
$delegate = $this->delegates[$id];
if (is_null($delegate[1])) {
$callback = $delegate[0];
} else {
$callback = $delegate;
}
try {
call_user_func_array($callback, $args);
} catch (Signals_BreakSignalException $ex) {
return FALSE;
}
}
}
return TRUE;
}
public function signalAll($subject) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
if (isset($this->subjects[$d])) {
$args = func_get_args();
array_shift($args);
foreach (array_keys($this->subjects[$d]) as $event) {
if (!$this->signalArgs($subject, $event, $args)) {
return FALSE;
}
}
}
return TRUE;
}
public function hasConnected($subject, $event) {
if (!is_object($subject)) {
throw new Exception("Wrong argument type. Subject must be an object.");
}
$d = $this->delegate($subject);
return isset($this->subjects[$d]) && count($this->subjects[$d]) > 0;
}
}
And a usecase:
PHP Code:
require_once 'signals.php';
echo "<pre>";
function myHandler() {
echo "myHandler called\n";
}
function myHandler2() {
echo "myHandler2 called\n";
}
$foo = new StdClass();
ob_start();
connect($foo, 'bar', 'myHandler');
signal($foo, 'bar');
if (assert(ob_get_clean() == "myHandler called\n")) {
echo "pass\n";
}
ob_start();
disconnect($foo, 'bar', 'myHandler');
signal($foo, 'bar');
if (assert(ob_get_clean() == "")) {
echo "pass\n";
}
ob_start();
Signals::$instance = new Signals();
connect($foo, 'bar', 'myHandler');
disconnectAll($foo, 'bar');
signal($foo, 'bar');
if (assert(ob_get_clean() == "")) {
echo "pass\n";
}
ob_start();
Signals::$instance = new Signals();
connect($foo, 'bar', 'myHandler');
connect($foo, 'bar', 'myHandler2');
signal($foo, 'bar');
if (assert(ob_get_clean() == "myHandler called\n"."myHandler2 called\n")) {
echo "pass\n";
}
ob_start();
Signals::$instance = new Signals();
connect($foo, 'bar', 'myHandler');
connect($foo, 'jazz', 'myHandler2');
signal($foo, 'bar');
if (assert(ob_get_clean() == "myHandler called\n")) {
echo "pass\n";
}
ob_start();
Signals::$instance = new Signals();
connect($foo, 'bar', 'myHandler');
connect($foo, 'bar', 'myHandler2');
disconnect($foo, 'bar', 'myHandler');
signal($foo, 'bar');
if (assert(ob_get_clean() == "myHandler2 called\n")) {
echo "pass\n";
}
$arr = Array(1,2,3,4);
$ex = NULL;
try {
connect($arr, 'bar', 'myHandler');
} catch (Exception $ex2) {
$ex = $ex2;
}
if (assert($ex2 instanceOf Exception && $ex2->getMessage() == "Wrong argument type. Subject must be an object.")) {
echo "pass\n";
}
The point being that you can basically subscribe to an event on any object, without the particular object having to implement any special code.
I'm not sure how relevant this is to you, but in case you're going on with the event/notify strategy, it might help. I should note that I didn't try this in a real application yet, so use at your own risk. In particular, this would prevent any kind of garbage collection by the PHP runtime. But since that's totally broken in the first place, it may not make much of a difference.
Bookmarks