. */ /** * Pt object definition file. * * Pt object is the base class used in Pt, all other classes are descendents of Pt object. * It also implements the signal and slot logic. * * @author Dinu Florin * @package Core */ /** * Pt object class. * * This is the base class, every other class is a descendent of Pt object. * This class implements the signal and slot logic. If you want your objects * to use signals and slots you have to extend this class. * * @author Dinu Florin * @package Core * */ class Pt { //The maximum number of recursions that emit will run const RecursionLevel = 500; /**#@+ * Signals, slots and connections * @ignore */ private static $signals = array(); private static $slots = array(); /**#@-*/ /**#@+ * Constants used to build the connections tree * @ignore */ const _Instance = 1; const _Static = 2; const _Object = 3; const _Receiver = 4; const _Emitter = 5; const _Slot = 6; const _Signal = 7; const _Params = 8; const _Connections = 9; const _Type = 10; /**#@-*/ /**#@+ * The class instance id. * @ignore */ private $clsIId = 0; private static $instanceCount = 0; /**#@-*/ /** * Get the class instance id. * The class instance id is a unique id used to identify an instance of this class. * * @return string */ public function getClsIId() { if($this->clsIId == 0) $this->clsIId = md5(get_class($this).((string)self::$instanceCount)); return $this->clsIId; } /** * Register a signal. * * Call this method in the constructor of your class to register a signal. The signal needs to be * defined, to define it just add an empty method with the same name and the same parameters to * your class. This method needs to be declared private or protected else registration will fail. * The method will never be called. * * @param mixed $emitter The emitter, this will be the class instance ($this) for instance methods and the class name for static methods. * @param string $signal The signal name. */ protected static function registerSignal($emitter, $signal) { assert('is_string($emitter) or is_object($emitter)'); assert('is_string($signal)'); $className = is_object($emitter)?get_class($emitter):$emitter; //Some debugging code if(DEBUG) { if(!method_exists($emitter, $signal)) { throw new PtException("Trying to register undefined signal {$className}::{$signal}"); } $r = new ReflectionMethod($className, $signal); if(!$r->isPrivate() && !$r->isProtected()) { throw new PtException("Signal {$className}::{$signal} must be declared private or protected"); } if($r->isStatic() && is_object($emitter)) { throw new PtException("Signal {$className}::{$signal} is declared static, but the registration request is using an object for the emitter instead of a class name."); } $numParams = count($r->getParameters()); } else $numParams = 0; self::$signals[$className][$signal] = array(self::_Params=>$numParams, self::_Connections=>array(), self::_Type=>is_object($emitter)?self::_Instance:self::_Static ); } /** * Register a slot. * * Call this method in the constructor of your class to register a slot. The slot is just a normal method * of your class but it needs to be defined at compile time (dynamic methods implemented with __call() will not work), and * it is recommended to have a fixed number of parameters (eg. don't use func_get_args() in the implementation). * * The slot will be called when a signal to which it's connected gets emitted, or you can use it as a normal method. * * @param mixed $receiver The receiver, this will be the class instance ($this) for instance methods and the class name for static methods. * @param string $slot The slot name. */ protected static function registerSlot($receiver, $slot) { assert('is_string($receiver) or is_object($receiver)'); assert('is_string($slot)'); $className = is_object($receiver)?get_class($receiver):$receiver; if(DEBUG) { if(!method_exists($receiver, $slot)) { throw new PtException("Trying to register undefined slot {$className}::{$slot}"); } $r = new ReflectionMethod($className, $slot); if($r->isStatic() && is_object($receiver)) { throw new PtException("Slot {$className}::{$slot} is declared static, but the registration request is using an object for the receiver instead of a class name."); } $numParams = count($r->getParameters()); } else $numParams = 0; self::$slots[$className][$slot] = array(self::_Params=>$numParams, self::_Type=>is_object($receiver)?self::_Instance:self::_Static ); } /** * Connect a slot to a signal. * * This method connects a slot (or a signal) to a signal. When the signal gets emitted the slot method is * called with the parameters passed to emit or in case $slot is also a signal that signal gets re-emitted * so that the slots connected to it get called. It is recommended that the slot and signal have the same number * and type of parameters, if they don't match a warning gets triggered. * * The signals and slots passed to this function need to be defined and registered. * @see Pt::registerSignal() * @see Pt::registerSlot() * * @param object $emitter The object that emits the signal. This will be the class instance ($this) or the class name. * @param string $signal The signal name. * @param object $receiver The object that has the slot (receives the signal). This will be the class instance ($this) or the class name. * @param string $slot The slot name. */ public static function connect($emitter, $signal, $receiver, $slot) { assert('is_string($emitter) or is_object($emitter)'); assert('is_string($signal)'); assert('is_string($receiver) or is_object($receiver)'); assert('is_string($slot)'); $emitterClass = is_object($emitter)?get_class($emitter):$emitter; $receiverClass = is_object($receiver)?get_class($receiver):$receiver; if(DEBUG && !isset(self::$signals[$emitterClass][$signal])) { throw new PtException("Trying to connect unknown signal {$emitterClass}::{$signal} to slot {$receiverClass}::{$slot}"); } //See if we are connecting a signal to a slot or a signal to another signal if(isset(self::$slots[$receiverClass][$slot])) { if(DEBUG) { if(is_object($receiver) && self::$slots[$receiverClass][$slot][self::_Type] == self::_Static) { throw new PtException("Trying to connect static slot {$receiverClass}::{$slot} but using an object as a receiver."); } elseif(!is_object($receiver) && self::$slots[$receiverClass][$slot][self::_Type] == self::_Instance) { throw new PtException("Trying to connect object slot {$receiverClass}::{$slot} but using a class name as a receiver instead of an object."); } } //We have a signal to slot connection //Build the node $node = array(); $node[self::_Receiver] = $receiver; $node[self::_Slot] = $slot; $node[self::_Type] = is_object($receiver)?self::_Instance:self::_Static; self::$signals[$emitterClass][$signal][self::_Connections][$receiverClass.$slot] = $node; //Check if the signal parameter count is the same as the slot's parameter count if(DEBUG && self::$slots[$receiverClass][$slot][self::_Params] != self::$signals[$emitterClass][$signal][self::_Params]) { throw new PtException("Connecting mismatched signal and slot: {$emitterClass}::{$signal} to {$receiverClass}::{$slot}"); } } elseif(isset(self::$signals[$receiverClass][$slot])) { if($receiverClass.$slot == $emitterClass.$signal) { throw new PtException("You can't connect signal {$emitterClass}::{$signal} to it's self"); } if(DEBUG) { if(is_object($receiver) && self::$signals[$receiverClass][$slot][self::_Type] == self::_Static) { throw new PtException("Trying to connect static signal {$receiverClass}::{$slot} but using an object as a receiver."); } elseif(!is_object($receiver) && self::$signals[$receiverClass][$slot][self::_Type] == self::_Instance) { throw new PtException("Trying to connect object signal {$receiverClass}::{$slot} but using a class name as a receiver instead of an object."); } } //We have a signal to signal connection //Build the node $node = array(); $node[self::_Receiver] = $receiver; $node[self::_Signal] = $slot; //this actually is a signal.. $node[self::_Type] = is_object($receiver)?self::_Instance:self::_Static; self::$signals[$emitterClass][$signal][self::_Connections][$receiverClass.$slot] = $node; //Check if the signals have the same number of parameters // if(DEBUG && self::$signals[$receiverClass][$slot][self::_Params] != self::$signals[$emitterClass][$signal][self::_Params]) // { // throw new PtException("Connecting mismatched signal and signal: {$emitterClass}::{$signal} to {$receiverClass}::{$slot}"); // } } else //The receiver is unknown, probably the programmer forgot to register the slot or signal passed as the receiver { throw new PtException("Trying to connect signal {$emitterClass}::{$signal} to unknown slot or signal {$receiverClass}::{$slot}"); } } /** * Disconnect a slot from a signal. * * @param mixed $emitter The emitter. * @param string $signal A registered signal. * @param mixed $receiver The receiver * @param string $slot A registered slot. */ public static function disconnect($emitter, $signal, $receiver, $slot) { $emitterClass = is_object($emitter)?get_class($emitter):$emitter; $receiverClass = is_object($receiver)?get_class($receiver):$receiver; if(isset(self::$signals[$emitterClass][$signal][self::_Connections][$receiverClass.$slot])) { unset(self::$signals[$emitterClass][$signal][self::_Connections][$receiverClass.$slot]); } } /** * Emit a signal. * * Call this method to emit a signal. When a signal is emitted all the slots and signals connected to it * get triggered. After the first two mandatory parameters this function can receive any number of optional * parameters that get passed to the slots. * * The signal passed to this function needs to be defined and registered. * @see Pt::registerSignal() * @see Pt::registerSlot() * @see Pt::connect() * * @param mixed $emitter The emitter, this will be the class instance ($this) for instance methods and the class name for static methods. * @param string $signal The signal to emit. * @param mixed $arg,... Any number of parameters that get passed to the slots connected to this signal. */ protected static function emit($emitter, $signal) { static $numRecursions = 0; assert('is_string($emitter) or is_object($emitter)'); assert('is_string($signal)'); $emitterClass = is_object($emitter)?get_class($emitter):$emitter; //Get the arguments passed to emit and unset the first 2, those are $emitter and $signal which we already have $args = func_get_args(); unset($args[0]); unset($args[1]); //See if we have connections for this signal and loop through them calling the slots or //re-emitting the signals recursively if(isset(self::$signals[$emitterClass][$signal])) { foreach(self::$signals[$emitterClass][$signal][self::_Connections] as $slot) { $receiver = $slot[self::_Receiver]; if(isset($slot[self::_Slot])) { //We have a slot, call it directly $callee = array($slot[self::_Receiver], $slot[self::_Slot]); call_user_func_array($callee, $args); } elseif(isset($slot[self::_Signal])) { //We have a signal so we call the emit method again to call the slots attached to it //Rebuild the first 2 parameters $args[0] = $slot[self::_Receiver]; $args[1] = $slot[self::_Signal]; ksort($args); if(++$numRecursions >= self::RecursionLevel) { throw new PtException('Recursion level exceeded in signal chain. You either have a circular reference problem or you chained too many signals.'); } $calee = array($emitterClass, 'emit'); call_user_func_array($calee, $args); } } } else { throw new GenericException("Trying to emit unknown signal {$emitterClass}::{$signal}"); } $numRecursions = 0; } /** * Default setter. * * This method implements the Pt object's propertyes. Do not call this method, * it is called automatically by PHP. * * @param string $name The name of the property. * @param mixed $value The value to set. */ public function __set($name, $value) { $methodName = 'set'.ucfirst($name); if(method_exists($this, $methodName)) { call_user_func(array($this, $methodName), $value); } else trigger_error('Trying to set undefined property '.get_class($this)."::{$name}", E_USER_WARNING); } /** * Default getter. * * This method implements the Pt object's propertyes. Do not call this method, * it is called automatically by PHP. * * @param string $name The name of the property. * @return mixed The property value */ public function __get($name) { $methodName = 'get'.ucfirst($name); if(method_exists($this, $methodName)) { return call_user_func(array($this, $methodName)); } else trigger_error('Undefined property '.get_class($this)."::{$name}", E_USER_WARNING); } /** * Check to see if a property exists. * This is a "magic" method, do *not* call this directly, use isset(Object->property) instead. * * @param string $name The property name * @return boolean */ public function __isset($name) { $methodName = 'get'.ucfirst($name); return method_exists($this, $methodName); } /** * Unsetter. * It disconnects the signals and slots belonging to this instance. * unset($foo->connections) will disconnect everything, unset($foo->slots) will disconnect the slots and unset($foo->signals) will * disconnect the signals. * */ public function __unset($name) { switch($name) { case 'signals': self::disconnect($this); break; case 'slots': self::disconnect(NULL, NULL, $this); break; case 'connections': self::disconnect($this); self::disconnect(NULL, NULL, $this); break; } } /** * Constructor. */ public function __construct() { self::$instanceCount++; } /** * Destructor. * It disconnects the signals and slots belonging to this instance. */ public function __destruct() { $className = get_class($this); if(isset(self::$signals[$className][self::_Connections])) { foreach(self::$signals[$className][self::_Connections] as $slot=>$node) { if($node[self::_Type] == self::_Instance) { unset(self::$signals[$className][self::_Connections][$slot]); } } } foreach(self::$signals as $emitterClass=>$signal) { if(isset($signal[self::_Connections])) { foreach($signal[self::_Connections] as $slot=>$node) { if($node[self::_Type] == self::_Instance && $node[self::_Receiver] == $className) { unset(self::$signals[$emitterClass][self::_Connections][$slot]); } } } } } /** * Output a formatted list of signal-slot connections. * * @return string */ public static function dumpConnections() { foreach(self::$connections as $emitterClass=>$signals) { echo "\n", $emitterClass; foreach($signals as $signalName=>$nodes) { echo "\n- ", $signalName; foreach($nodes as $node) { $instance = $node[self::TReceiver][self::TObject]; $receiver = $node[self::TReceiver]; $slot = isset($receiver[self::TSlot])?$receiver[self::TSlot]:$receiver[self::TSignal]; $receiverClass = is_object($instance)?get_class($instance):$instance; echo "\n\t=>", $receiverClass, '::', $slot; } } } } } /** * Pt exception class * This exeption is thrown only by the Pt class. * * @package Core * @author Dinu Florin */ class PtException extends GenericException { protected $message = 'Pt exeption'; } ?>