diff options
-rw-r--r-- | .gitattributes | 3 | ||||
-rw-r--r-- | framework/Exceptions/messages/messages.txt | 4 | ||||
-rw-r--r-- | framework/TApplication.php | 4 | ||||
-rw-r--r-- | framework/TComponent.php | 1538 | ||||
-rw-r--r-- | framework/Util/TBehavior.php | 87 | ||||
-rw-r--r-- | framework/Util/TCallChain.php | 147 | ||||
-rw-r--r-- | framework/Util/TClassBehavior.php | 36 | ||||
-rw-r--r-- | tests/unit/TComponentTest.php | 1940 |
8 files changed, 3654 insertions, 105 deletions
diff --git a/.gitattributes b/.gitattributes index 6541f9fb..3102dd50 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2706,6 +2706,9 @@ framework/I18N/core/data/zh_TW.dat -text framework/I18N/schema/mysql.sql -text framework/I18N/schema/postgresql.sql -text framework/I18N/schema/sqlite.sql -text +framework/Util/TBehavior.php -text +framework/Util/TCallChain.php -text +framework/Util/TClassBehavior.php -text framework/Web/Javascripts/source/.htaccess -text framework/Web/Javascripts/source/prado/activefileupload/ActiveFileUploadBlank.html -text framework/Web/Javascripts/source/prado/activefileupload/ActiveFileUploadComplete.png -text svneol=unset#image/png diff --git a/framework/Exceptions/messages/messages.txt b/framework/Exceptions/messages/messages.txt index 69fd89fc..468ea4e1 100644 --- a/framework/Exceptions/messages/messages.txt +++ b/framework/Exceptions/messages/messages.txt @@ -11,6 +11,10 @@ component_event_undefined = Component event '{0}.{1}' is not defined. component_eventhandler_invalid = Component event '{0}.{1}' is attached with an invalid event handler '{2}'. component_expression_invalid = Component '{0}' is evaluating an invalid expression '{1}' : {2}. component_statements_invalid = Component '{0}' is evaluating invalid PHP statements '{1}' : {2}. +component_class_behavior_defined = Component '{0}' already has a class behavior of '{1}'. +component_not_a_behavior = Component '{0}' is being added as behavior is not a TBaseBehavior. +component_no_tcomponent_class_behaviors = TComponent cannot have class behaviors attached due to recursion. +component_no_class_provided_nor_late_binding = Adding or Removing Class Behaviors must have PHP feature Late Static Binding or a class provided as a parameter propertyvalue_enumvalue_invalid = Value '{0}' is a not valid enumeration value ({1}). diff --git a/framework/TApplication.php b/framework/TApplication.php index 1da70ba5..be43e697 100644 --- a/framework/TApplication.php +++ b/framework/TApplication.php @@ -24,8 +24,8 @@ Prado::using('System.TService'); Prado::using('System.Exceptions.TErrorHandler'); Prado::using('System.Caching.TCache'); Prado::using('System.IO.TTextWriter'); -Prado::using('System.Collections.TList'); -Prado::using('System.Collections.TMap'); +Prado::using('System.Collections.TPriorityList'); +Prado::using('System.Collections.TPriorityMap'); Prado::using('System.Collections.TStack'); Prado::using('System.Xml.TXmlDocument'); Prado::using('System.Security.TAuthorizationRule'); diff --git a/framework/TComponent.php b/framework/TComponent.php index 941f2c50..930afe23 100644 --- a/framework/TComponent.php +++ b/framework/TComponent.php @@ -1,10 +1,13 @@ <?php /** * TComponent, TPropertyValue classes - * * @author Qiang Xue <qiang.xue@gmail.com> + * + * Global Events, intra-object events, Class behaviors, expanded behaviors + * @author Brad Anderson <javalizard@mac.com> + * * @link http://www.pradosoft.com/ - * @copyright Copyright © 2005-2012 PradoSoft + * @copyright Copyright © 2005-2011 PradoSoft * @license http://www.pradosoft.com/license/ * @version $Id$ * @package System @@ -14,7 +17,8 @@ * TComponent class * * TComponent is the base class for all PRADO components. - * TComponent implements the protocol of defining, using properties and events. + * TComponent implements the protocol of defining, using properties, behaviors, + * and events. * * A property is defined by a getter method, and/or a setter method. * Properties can be accessed in the way like accessing normal object members. @@ -35,6 +39,8 @@ * in the format of concatenated words, with the first letter of each word * capitalized (e.g. DisplayMode, ItemStyle). * + * Javascript Get and Set + * * Since Prado 3.2 a new class of javascript-friendly properties have been introduced * to better deal with potential security problems like cross-site scripting issues. * All the data that gets sent clientside inside a javascript block is now encoded by default. @@ -60,6 +66,8 @@ * statement and will not be encoded when rendered inside a javascript block. * This special handling makes use of the {@link TJavaScriptLiteral} class. * + * Events + * * An event is defined by the presence of a method whose name starts with 'on'. * The event name is the method name and is thus case-insensitive. * An event can be attached with one or several methods (called event handlers). @@ -74,6 +82,7 @@ * To raise an event (assuming named as 'Click') of a component, use * <code> * $component->raiseEvent('OnClick'); + * $component->raiseEvent('OnClick', $this, $param); * </code> * To attach an event handler to an event, use one of the following ways, * <code> @@ -81,7 +90,7 @@ * $component->attachEventHandler('OnClick',$callback); * </code> * The first two ways make use of the fact that $component->OnClick refers to - * the event handler list {@link TList} for the 'OnClick' event. + * the event handler list {@link TPriorityList} for the 'OnClick' event. * The variable $callback contains the definition of the event handler that can * be either a string referring to a global function name, or an array whose * first element refers to an object and second element a method name/path that @@ -90,11 +99,187 @@ * - array($object,'buttonClicked') : $object->buttonClicked($sender,$param); * - array($object,'MainContent.SubmitButton.buttonClicked') : * $object->MainContent->SubmitButton->buttonClicked($sender,$param); - * + * * @author Qiang Xue <qiang.xue@gmail.com> + * + * With the addition of behaviors, a more expansive event model is needed. There + * are two new event types (global and dynamic events) as well as a more comprehensive + * behavior model that includes class wide behaviors. + * + * A global event is defined by all events whose name starts with 'fx'. + * The event name is potentially a method name and is thus case-insensitive. All 'fx' events + * are valid as the whole 'fx' event/method space is global in nature. Any object may patch into + * any global event by defining that event as a method. Global events have priorities + * just like 'on' events; so as to be able to order the event execution. Due to the + * nature of all events which start with 'fx' being valid, in effect, every object + * has every 'fx' global event. It is simply an issue of tapping into the desired + * global event. + * + * A global event that starts with 'fx' can be called even if the object does not + * implement the method of the global event. A call to a non-existing 'fx' method + * will, at minimal, function and return null. If a method argument list has a first + * parameter, it will be returned instead of null. This allows filtering and chaining. + * 'fx' methods do not automatically install and uninstall. To install and uninstall an + * object's global event listeners, call the object's {@link listen} and + * {@link unlisten} methods, respectively. An object may auto-install its global event + * during {@link __construct} by overriding {@link getAutoGlobalListen} and returning true. + * + * As of PHP version 5.3, nulled objects without code references will still continue to persist + * in the global event queue because {@link __destruct} is not automatically called. In the common + * __destruct method, if an object is listening to global events, then {@link unlisten} is called. + * {@link unlisten} is required to be manually called before an object is + * left without references if it is currently listening to any global events. This includes + * class wide behaviors. + * + * An object that contains a method that starts with 'fx' will have those functions + * automatically receive those events of the same name after {@link listen} is called on the object. + * + * An object may listen to a global event without defining an 'fx' method of the same name by + * adding an object method to the global event list. For example + * <code> + * $component->fxGlobalCheck=$callback; // or $component->OnClick->add($callback); + * $component->attachEventHandler('fxGlobalCheck',array($object, 'someMethod')); + * </code> + * + * Events between Objects and their behaviors, Dynamic Events + * + * An intra-object/behavior event is defined by methods that start with 'dy'. Just as with + * 'fx' global events, every object has every dynamic event. Any call to a method that + * starts with 'dy' will be handled, regardless of whether it is implemented. These + * events are for communicating with attached behaviors. + * + * Dynamic events can be used in a variety of ways. They can be used to tell behaviors + * when a non-behavior method is called. Dynamic events could be used as data filters. + * They could also be used to specify when a piece of code is to be run, eg. should the + * loop process be performed on a particular piece of data. In this way, some control + * is handed to the behaviors over the process and/or data. + * + * If there are no handlers for an 'fx' or 'dy' event, it will return the first + * parameter of the argument list. If none there are no arguments, these events + * will return null. If there are handlers an 'fx' method will be called directly + * within the object. Global 'fx' events are triggered by calling {@link raiseEvent}. + * For dynamic events where there are behaviors that respond to the dynamic events, a + * {@link TCallChain} is developed. A call chain allows the behavior dynamic event + * implementations to call further implementing behaviors within a chain. + * + * If an object implements {@link IDynamicMethods}, all global and object dynamic + * events will be sent to {@link __dycall}. In the case of global events, all + * global events will trigger this method. In the case of behaviors, all undefined + * dynamic events which are called will be passed through to this method. + * + * + * Behaviors + * + * There are two types of behaviors. There are individual object behaviors and + * there are class wide behaviors. Class behaviors depend upon object behaviors. + * + * When a new class implements {@link IBehavior} or {@link IClassBehavior} or + * extends {@link TBehavior} or {@link TClassBehavior}, it may be added to an + * object by calling the object's {@link attachBehavior}. The behaviors associated + * name can then be used to {@link enableBehavior} or {@link disableBehavior} + * the specific behavior. + * + * All behaviors may be turned on and off via {@link enableBehaviors} and + * {@link disableBehaviors}, respectively. To check if behaviors are on or off + * a call to {@link getBehaviorsEnabled} will provide the variable. + * + * Attaching and detaching whole sets of behaviors is done using + * {@link attachBehaviors} and {@link detachBehaviors}. {@link clearBehaviors} + * removes all of an object's behaviors. + * + * {@link asa} returns a behavior of a specific name. {@link isa} is the + * behavior inclusive function that acts as the PHP operator {@link instanceof}. + * A behavior could provide the functionality of a specific class thus causing + * the host object to act similarly to a completely different class. A behavior + * would then implement {@link IInstanceCheck} to provide the identity of the + * different class. + * + * Class behaviors are similar to object behaviors except that the class behavior + * is the implementation for all instances of the class. A class behavior + * will have the object upon which is being called be prepended to the parameter + * list. This way the object is known across the class behavior implementation. + * + * Class behaviors are attached using {@link attachClassBehavior} and detached + * using {@link detachClassBehavior}. Class behaviors are important in that + * they will be applied to all new instances of a particular class. In this way + * class behaviors become default behaviors to a new instances of a class in + * {@link __construct}. Detaching a class behavior will remove the behavior + * from the default set of behaviors created for an object when the object + * is instanced. + * + * Class behaviors are also added to all existing instances via the global 'fx' + * event mechanism. When a new class behavior is added, the event + * {@link fxAttachClassBehavior} is raised and all existing instances that are + * listening to this global event (primarily after {@link listen} is called) + * will have this new behavior attached. A similar process is used when + * detaching class behaviors. Any objects listening to the global 'fx' event + * {@link fxDetachClassBehavior} will have a class behavior removed. + * + * Dynamic Intra-Object Events + * + * Dynamic events start with 'dy'. This mechanism is used to allow objects + * to communicate with their behaviors directly. The entire 'dy' event space + * is valid. All attached, enabled behaviors that implement a dynamic event + * are called when the host object calls the dynamic event. If there is no + * implementation or behaviors, this returns null when no parameters are + * supplied and will return the first parameter when there is at least one + * parameter in the dynamic event. + * <code> + * null == $this->dyBehaviorEvent(); + * 5 == $this->dyBehaviorEvent(5); //when no behaviors implement this dynamic event + * </code> + * + * Dynamic events can be chained together within behaviors to allow for data + * filtering. Dynamic events are implemented within behaviors by defining the + * event as a method. + * <code> + * class TObjectBehavior extends TBehavior { + * public function dyBehaviorEvent($param1, $callchain) { + * //Do something, eg: $param1 += 13; + * return $callchain->dyBehaviorEvent($param1); + * } + * } + * </code> + * This implementation of a behavior and dynamic event will flow through to the + * next behavior implementing the dynamic event. The first parameter is always + * return when it is supplied. Otherwise a dynamic event returns null. + * + * In the case of a class behavior, the object is also prepended to the dynamic + * event. + * <code> + * class TObjectClassBehavior extends TClassBehavior { + * public function dyBehaviorEvent($hostobject, $param1, $callchain) { + * //Do something, eg: $param1 += $hostobject->getNumber(); + * return $callchain->dyBehaviorEvent($param1); + * } + * } + * </code> + * When calling a dynamic event, only the parameters are passed. The host object + * and the call chain are built into the framework. + * + * Global Event and Dynamic event catching + * + * Given that all global 'fx' events and dynamic 'dy' events are valid and + * operational, there is a mechanism for catching events called that are not + * implemented (similar to the built-in PHP method {@link __call}). When + * a dynamic or global event is called but a behavior does not implement it, + * yet desires to know when an undefined dynamic event is run, the behavior + * implements the interface {@link IDynamicMethods} and method {@link __dycall}. + * + * In the case of dynamic events, {@link __dycall} is supplied with the method + * name and its parameters. When a global event is raised, via {@link raiseEvent}, + * the method is the event name and the parameters are supplied. + * + * When implemented, this catch-all mechanism is called for event global event event + * when implemented outside of a behavior. Within a behavior, it will also be called + * when the object to which the behavior is attached calls any unimplemented dynamic + * event. This is the fall-back mechanism for informing a class and/or behavior + * of when an global and/or undefined dynamic event is executed. + * + * @author Brad Anderson <javalizard@mac.com> * @version $Id$ * @package System - * @since 3.0 + * @since 3.1.8 */ class TComponent { @@ -102,6 +287,279 @@ class TComponent * @var array event handler lists */ private $_e=array(); + + /** + * @var boolean if listening is enabled. Automatically turned on or off in + * constructor according to {@link getAutoGlobalListen}. Default false, off + */ + private $_listeningenabled=false; + + /** + * @var array static registered global event handler lists + */ + private static $_ue=array(); + + /** + * @var boolean if object behaviors are on or off. default true, on + */ + private $_behaviorsenabled=true; + + /** + * @var TPriorityMap list of object behaviors + */ + private $_m=null; + + /** + * @var array static global class behaviors, these behaviors are added upon instantiation of a class + */ + private static $_um=array(); + + + /** + * @const string the name of the global {@link raiseEvent} listener + */ + const GLOBAL_RAISE_EVENT_LISTENER='fxGlobalListener'; + + + /** + * The common __construct + * If desired by the new object, this will auto install and listen to global event functions + * as defined by the object via 'fx' methods. This also attaches any predefined behaviors. + * This function installs all class behaviors in a class hierarchy from the deepest subclass + * through each parent to the top most class, TComponent. + */ + public function __construct() { + if($this->getAutoGlobalListen()) + $this->listen(); + + $classes=array_reverse($this->getClassHierarchy(true)); + foreach($classes as $class) { + if(isset(self::$_um[$class])) + $this->attachBehaviors(self::$_um[$class]); + } + } + + + /** + * Tells TComponent whether or not to automatically listen to global events. + * Defaults to false because PHP variable cleanup is affected if this is true. + * When unsetting a variable that is listening to global events, {@link unlisten} + * must explicitly be called when cleaning variables allocation or else the global + * event registry will contain references to the old object. This is true for PHP 5.4 + * + * Override this method by a subclass to change the setting. When set to true, this + * will enable {@link __construct} to call {@link listen}. + * + * @return boolean whether or not to auto listen to global events during {@link __construct}, default false + */ + public function getAutoGlobalListen() { + return false; + } + + + /** + * The common __destruct + * This unlistens from the global event routines if listening + * + * PHP 5.3 does not __destruct objects when they are nulled and thus unlisten must be + * called must be explicitly called. + */ + public function __destruct() { + if($this->_listeningenabled) + $this->unlisten(); + } + + + /** + * This utility function is a private array filter method. The array values + * that start with 'fx' are filtered in. + */ + private function filter_prado_fx($name) { + return strncasecmp($name,'fx',2)===0; + } + + + /** + * This returns an array of the class name and the names of all its parents. The base object first, + * {@link TComponent}, and the deepest subclass is last. + * @param boolean optional should the names be all lowercase true/false + * @return array array of strings being the class hierarchy of $this. + */ + public function getClassHierarchy($lowercase = false) + { + $class=get_class($this); + $classes=array($class); + while($class=get_parent_class($class)){array_unshift($classes,$class);} + if($lowercase) + return array_map('strtolower',$classes); + return $classes; + } + + + /** + * This adds an object's fx event handlers into the global broadcaster to listen into any + * broadcast global events called through {@link raiseEvent} + * + * Behaviors may implement the function: + * <code> + * public function dyListen($globalEvents[, $chain]) { + * $this->listen(); //eg + * } + * </code> + * to be executed when listen is called. All attached behaviors are notified through dyListen. + * + * @return numeric the number of global events that were registered to the global event registry + */ + public function listen() { + if($this->_listeningenabled) + return; + + $fx=array_filter(get_class_methods($this),array($this,'filter_prado_fx')); + + foreach($fx as $func) + $this->attachEventHandler($func,array($this,$func)); + + if(is_a($this,'IDynamicMethods')) { + $this->attachEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER,array($this,'__dycall')); + array_push($fx,TComponent::GLOBAL_RAISE_EVENT_LISTENER); + } + + $this->_listeningenabled=true; + + $this->dyListen($fx); + + return count($fx); + } + + /** + * this removes an object's fx events from the global broadcaster + * + * Behaviors may implement the function: + * <code> + * public function dyUnlisten($globalEvents[, $chain]) { + * $this->behaviorUnlisten(); //eg + * } + * </code> + * to be executed when listen is called. All attached behaviors are notified through dyUnlisten. + * + * @return numeric the number of global events that were unregistered from the global event registry + */ + public function unlisten() { + if(!$this->_listeningenabled) + return; + + $fx=array_filter(get_class_methods($this),array($this,'filter_prado_fx')); + + foreach($fx as $func) + $this->detachEventHandler($func,array($this,$func)); + + if(is_a($this,'IDynamicMethods')) { + $this->detachEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER,array($this,'__dycall')); + array_push($fx,TComponent::GLOBAL_RAISE_EVENT_LISTENER); + } + + $this->_listeningenabled=false; + + $this->dyUnlisten($fx); + + return count($fx); + } + + /** + * Gets the state of listening to global events + * @return boolean is Listening to global broadcast enabled + */ + public function getListeningToGlobalEvents() + { + return $this->_listeningenabled; + } + + + /** + * Calls a method. + * Do not call this method directly. This is a PHP magic method that we override + * to allow behaviors, dynamic events (intra-object/behavior events), + * undefined dynamic and global events, and + * to allow using the following syntax to call a property setter or getter. + * <code> + * $this->getPropertyName($value); // if there's a $this->getjsPropertyName() method + * $this->setPropertyName($value); // if there's a $this->setjsPropertyName() method + * </code> + * + * Additional object behaviors override class behaviors. + * dynamic and global events do not fail even if they aren't implemented. + * Any intra-object/behavior dynamic events that are not implemented by the behavior + * return the first function paramater or null when no parameters are specified. + * + * @param string method name that doesn't exist and is being called on the object + * @param mixed method parameters + * @throws TInvalidOperationException If the property is not defined or read-only or + * method is undefined + * @return mixed result of the method call, or false if 'fx' or 'dy' function but + * is not found in the class, otherwise it runs + */ + public function __call($method, $args) + { + $getset=substr($method,0,3); + if(($getset=='get')||($getset=='set')) + { + $propname=substr($method,3); + $jsmethod=$getset.'js'.$propname; + if(method_exists($this,$jsmethod)) + { + if(count($args)>0) + if($args[0]&&!($args[0] instanceof TJavaScriptString)) + $args[0]=new TJavaScriptString($args[0]); + return call_user_func_array(array($this,$jsmethod),$args); + } + + if (($getset=='set')&&method_exists($this,'getjs'.$propname)) + throw new TInvalidOperationException('component_property_readonly',get_class($this),$method); + } + + if($this->_m!==null&&$this->_behaviorsenabled) + { + if(strncasecmp($method,'dy',2)===0) + { + $callchain=new TCallChain($method); + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&(method_exists($behavior,$method)||($behavior instanceof IDynamicMethods))) + { + $behavior_args=$args; + if($behavior instanceof IClassBehavior) + array_unshift($behavior_args,$this); + $callchain->addCall(array($behavior,$method),$behavior_args); + } + + } + if($callchain->getCount()>0) + return call_user_func_array(array($callchain,'call'),$args); + } + else + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&method_exists($behavior,$method)) + { + if($behavior instanceof IClassBehavior) + array_unshift($args,$this); + return call_user_func_array(array($behavior,$method),$args); + } + } + } + } + + if(strncasecmp($method,'dy',2)===0||strncasecmp($method,'fx',2)===0) + { + if($this instanceof IDynamicMethods) + return $this->__dycall($method,$args); + return isset($args[0])?$args[0]:null; + } + + throw new TApplicationException('component_method_undefined',get_class($this),$method); + } + /** * Returns a property value or an event handler list by property or event name. @@ -115,21 +573,27 @@ class TComponent * <code> * $eventHandlerList=$component->EventName; * </code> + * This will also return the global event handler list when specifing an 'fx' + * event, + * <code> + * $globalEventHandlerList=$component->fxEventName; + * </code> + * When behaviors are enabled, this will return the behavior of a specific + * name, a property of a behavior, or an object 'on' event defined by the behavior. * @param string the property name or the event name - * @return mixed the property value or the event handler list + * @return mixed the property value or the event handler list as {@link TPriorityList} * @throws TInvalidOperationException if the property/event is not defined. */ public function __get($name) { - $getter='get'.$name; $jsgetter = 'getjs'.$name; - if(method_exists($this,$getter)) + if(method_exists($this,$getter='get'.$name)) { // getting a property return $this->$getter(); } - else if(method_exists($this,$jsgetter)) + else if(method_exists($this,$jsgetter='getjs'.$name)) { - // getting a property + // getting a javascript property return (string)$this->$jsgetter(); } else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) @@ -137,13 +601,33 @@ class TComponent // getting an event (handler list) $name=strtolower($name); if(!isset($this->_e[$name])) - $this->_e[$name]=new TList; + $this->_e[$name]=new TPriorityList; return $this->_e[$name]; } - else + else if(strncasecmp($name,'fx',2)===0) { - throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); + // getting a global event (handler list) + $name=strtolower($name); + if(!isset(self::$_ue[$name])) + self::$_ue[$name]=new TPriorityList; + return self::$_ue[$name]; } + else if($this->_behaviorsenabled) + { + // getting a behavior property/event (handler list) + if(isset($this->_m[$name])) + return $this->_m[$name]; + else if($this->_m!==null) + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&& + (property_exists($behavior,$name)||$behavior->canGetProperty($name)||$behavior->hasEvent($name))) + return $behavior->$name; + } + } + } + throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); } /** @@ -154,30 +638,47 @@ class TComponent * $this->PropertyName=$value; * $this->jsPropertyName=$value; // $value will be treated as a JavaScript literal * $this->EventName=$handler; + * $this->fxEventName=$handler; //global event listener * </code> + * When behaviors are enabled, this will also set a behaviors properties and events. * @param string the property name or event name * @param mixed the property value or event handler * @throws TInvalidOperationException If the property is not defined or read-only. */ public function __set($name,$value) { - if(method_exists($this, $setter='set'.$name)) + if(method_exists($this,$setter='set'.$name)) { - if (strncasecmp($name,'js',2)===0 && $value && !($value instanceof TJavaScriptLiteral)) + if(strncasecmp($name,'js',2)===0&&$value&&!($value instanceof TJavaScriptLiteral)) $value = new TJavaScriptLiteral($value); - $this->$setter($value); + return $this->$setter($value); } - else if(method_exists($this, $jssetter = 'setjs'.$name)) + else if(method_exists($this,$jssetter='setjs'.$name)) { - if ($value and !($value instanceof TJavaScriptString)) - $value = new TJavaScriptString($value); - $this->$jssetter($value); + if($value&&!($value instanceof TJavaScriptString)) + $value=new TJavaScriptString($value); + return $this->$jssetter($value); } - else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) + else if((strncasecmp($name,'on',2)===0&&method_exists($this,$name))||strncasecmp($name,'fx',2)===0) { - $this->attachEventHandler($name,$value); + return $this->attachEventHandler($name,$value); } - else if(method_exists($this,'get'.$name) || method_exists($this,'getjs'.$name)) + else if($this->_m!==null&&$this->_m->getCount()>0&&$this->_behaviorsenabled) + { + $sets=0; + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&& + (property_exists($behavior,$name)||$behavior->canSetProperty($name)||$behavior->hasEvent($name))) { + $behavior->$name=$value; + $sets++; + } + } + if($sets)return $value; + + } + + if(method_exists($this,'get'.$name)||method_exists($this,'getjs'.$name)) { throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); } @@ -188,37 +689,86 @@ class TComponent } /** - * Calls a method. + * Checks if a property value is null, there are no events in the object + * event list or global event list registered under the name, and, if + * behaviors are enabled, * Do not call this method. This is a PHP magic method that we override - * to allow using the following syntax to call a property setter or getter. - * <code> - * $this->getPropertyName($value); // if there's a $this->getjsPropertyName() method - * $this->setPropertyName($value); // if there's a $this->setjsPropertyName() method - * </code> - * @param string the getter or setter method name - * @param mixed method call parameters - * @throws TInvalidOperationException If the property is not defined or read-only. + * to allow using isset() to detect if a component property is set or not. + * This also works for global events. When behaviors are enabled, it + * will check for a behavior of the specified name, and also check + * the behavior for events and properties. + * @param string the property name or the event name + * @since 1.0.1 */ - public function __call($name,$params) + public function __isset($name) { - $getset = substr($name,0,3); - if (($getset=='get') || ($getset=='set')) + if(method_exists($this,$getter='get'.$name)) + return $this->$getter()!==null; + else if(method_exists($this,$jsgetter='getjs'.$name)) + return $this->$jsgetter()!==null; + else if(strncasecmp($name,'on',2)===0&&method_exists($this,$name)) { - $propname = substr($name,3); - $jsmethod = $getset.'js'.$propname; - if (method_exists($this, $jsmethod)) + $name=strtolower($name); + return isset($this->_e[$name])&&$this->_e[$name]->getCount(); + } + else if(strncasecmp($name,'fx',2)===0) + { + $name=strtolower($name); + return isset(self::$_ue[$name])&&self::$_ue[$name]->getCount(); + } + else if($this->_m!==null&&$this->_m->getCount()>0&&$this->_behaviorsenabled) + { + if(isset($this->_m[$name])) + return true; + foreach($this->_m->toArray() as $behavior) { - if (count($params)>0) - if ($params[0] && !($params[0] instanceof TJavaScriptString)) - $params[0] = new TJavaScriptString($params[0]); - return call_user_func_array(array($this, $jsmethod), $params); + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())) + return isset($behavior->$name); } - - if (($getset=='set') and method_exists($this, 'getjs'.$propname)) - throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); + } + else + return false; + } - throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); + /** + * Sets a component property to be null. Clears the object or global + * events. When enabled, loops through all behaviors and unsets the + * property or event. + * Do not call this method. This is a PHP magic method that we override + * to allow using unset() to set a component property to be null. + * @param string the property name or the event name + * @throws TInvalidOperationException if the property is read only. + * @since 3.1.8 + */ + public function __unset($name) + { + if(method_exists($this,$setter='set'.$name)) + $this->$setter(null); + else if(method_exists($this,$jssetter='setjs'.$name)) + $this->$jssetter(null); + else if(strncasecmp($name,'on',2)===0&&method_exists($this,$name)) + $this->_e[strtolower($name)]->clear(); + else if(strncasecmp($name,'fx',2)===0) + $this->getEventHandlers($name)->remove(array($this, $name)); + else if($this->_m!==null&&$this->_m->getCount()>0&&$this->_behaviorsenabled) + { + if(isset($this->_m[$name])) + $this->detachBehavior($name); + else { + $unset=0; + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())) { + unset($behavior->$name); + $unset++; + } + } + if(!$unset&&method_exists($this,'get'.$name)) + throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); + } + } else if(method_exists($this,'get'.$name)) + throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); } /** @@ -230,34 +780,55 @@ class TComponent */ public function hasProperty($name) { - return - method_exists($this,'get'.$name) || method_exists($this,'set'.$name) || - method_exists($this,'getjs'.$name) || method_exists($this,'setjs'.$name) - ; + return $this->canGetProperty($name)||$this->canSetProperty($name); } /** * Determines whether a property can be read. * A property can be read if the class has a getter method * for the property name. Note, property name is case-insensitive. + * This also checks for getjs. When enabled, it loops through all + * active behaviors for the get property when undefined by the object. * @param string the property name * @return boolean whether the property can be read */ public function canGetProperty($name) { - return method_exists($this,'get'.$name) || method_exists($this,'getjs'.$name); + if(method_exists($this,'get'.$name)||method_exists($this,'getjs'.$name)) + return true; + else if($this->_m!==null&&$this->_behaviorsenabled) + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&$behavior->canGetProperty($name)) + return true; + } + } + return false; } /** * Determines whether a property can be set. * A property can be written if the class has a setter method * for the property name. Note, property name is case-insensitive. + * This also checks for setjs. When enabled, it loops through all + * active behaviors for the set property when undefined by the object. * @param string the property name * @return boolean whether the property can be written */ public function canSetProperty($name) { - return method_exists($this,'set'.$name) || method_exists($this,'setjs'.$name); + if(method_exists($this,'set'.$name)||method_exists($this,'setjs'.$name)) + return true; + else if($this->_m!==null&&$this->_behaviorsenabled) + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&$behavior->canSetProperty($name)) + return true; + } + } + return false; } /** @@ -265,6 +836,8 @@ class TComponent * A property path is a sequence of property names concatenated by '.' character. * For example, 'Parent.Page' refers to the 'Page' property of the component's * 'Parent' property value (which should be a component also). + * When a property is not defined by an object, this also loops through all + * active behaviors of the object. * @param string property path * @return mixed the property path value */ @@ -281,6 +854,8 @@ class TComponent * A property path is a sequence of property names concatenated by '.' character. * For example, 'Parent.Page' refers to the 'Page' property of the component's * 'Parent' property value (which should be a component also). + * When a property is not defined by an object, this also loops through all + * active behaviors of the object. * @param string property path * @param mixed the property path value */ @@ -299,41 +874,90 @@ class TComponent /** * Determines whether an event is defined. - * An event is defined if the class has a method whose name is the event name prefixed with 'on'. + * An event is defined if the class has a method whose name is the event name + * prefixed with 'on', 'fx', or 'dy'. + * Every object responds to every 'fx' and 'dy' event as they are in a universally + * accepted event space. 'on' event must be declared by the object. + * When enabled, this will loop through all active behaviors for 'on' events + * defined by the behavior. * Note, event name is case-insensitive. * @param string the event name * @return boolean */ public function hasEvent($name) { - return strncasecmp($name,'on',2)===0 && method_exists($this,$name); + if((strncasecmp($name,'on',2)===0&&method_exists($this,$name))||strncasecmp($name,'fx',2)===0||strncasecmp($name,'dy',2)===0) + return true; + + else if($this->_m!==null&&$this->_behaviorsenabled) + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&$behavior->hasEvent($name)) + return true; + } + } + return false; } /** + * Checks if an event has any handlers. This function also checks through all + * the behaviors for 'on' events when behaviors are enabled. + * 'dy' dynamic events are not handled by this function. + * @param string the event name * @return boolean whether an event has been attached one or several handlers */ public function hasEventHandler($name) { $name=strtolower($name); - return isset($this->_e[$name]) && $this->_e[$name]->getCount()>0; + if(strncasecmp($name,'fx',2)===0) + return isset(self::$_ue[$name])&&self::$_ue[$name]->getCount()>0; + else + { + if(isset($this->_e[$name])&&$this->_e[$name]->getCount()>0) + return true; + else if($this->_m!==null&&$this->_behaviorsenabled) { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&$behavior->hasEventHandler($name)) + return true; + } + } + } + return false; } /** - * Returns the list of attached event handlers for an event. - * @return TList list of attached event handlers for an event + * Returns the list of attached event handlers for an 'on' or 'fx' event. This function also + * checks through all the behaviors for 'on' event lists when behaviors are enabled. + * @return TPriorityList list of attached event handlers for an event * @throws TInvalidOperationException if the event is not defined */ public function getEventHandlers($name) { - if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) + if(strncasecmp($name,'on',2)===0&&method_exists($this,$name)) { $name=strtolower($name); if(!isset($this->_e[$name])) - $this->_e[$name]=new TList; + $this->_e[$name]=new TPriorityList; return $this->_e[$name]; } - else - throw new TInvalidOperationException('component_event_undefined',get_class($this),$name); + else if(strncasecmp($name,'fx',2)===0) + { + $name=strtolower($name); + if(!isset(self::$_ue[$name])) + self::$_ue[$name]=new TPriorityList; + return self::$_ue[$name]; + } + else if($this->_m!==null&&$this->_behaviorsenabled) + { + foreach($this->_m->toArray() as $behavior) + { + if((!($behavior instanceof IBehavior)||$behavior->getEnabled())&&$behavior->hasEvent($name)) + return $behavior->getEventHandlers($name); + } + } + throw new TInvalidOperationException('component_event_undefined',get_class($this),$name); } /** @@ -348,16 +972,18 @@ class TComponent * * The event handler must be of the following signature, * <code> - * function handlerName($sender,$param) {} + * function handlerName($sender, $param) {} + * function handlerName($sender, $param, $name) {} * </code> * where $sender represents the object that raises the event, - * and $param is the event parameter. + * and $param is the event parameter. $name refers to the event name + * being handled. * * This is a convenient method to add an event handler. * It is equivalent to {@link getEventHandlers}($name)->add($handler). * For complete management of event handlers, use {@link getEventHandlers} * to get the event handler list first, and then do various - * {@link TList} operations to append, insert or remove + * {@link TPriorityList} operations to append, insert or remove * event handlers. You may also do these operations like * getting and setting properties, e.g., * <code> @@ -370,29 +996,37 @@ class TComponent * $component->getEventHandlers('OnClick')->insertAt(0,array($object,'buttonClicked')); * </code> * + * Due to the nature of {@link getEventHandlers}, any active behaviors defining + * new 'on' events, this method will pass through to the behavior transparently. + * * @param string the event name * @param callback the event handler + * @param numeric|null the priority of the handler, defaults to null which translates into the + * default priority of 10.0 within {@link TPriorityList} * @throws TInvalidOperationException if the event does not exist */ - public function attachEventHandler($name,$handler) + public function attachEventHandler($name,$handler,$priority=null) { - $this->getEventHandlers($name)->add($handler); + $this->getEventHandlers($name)->add($handler,$priority); } /** * Detaches an existing event handler. - * This method is the opposite of {@link attachEventHandler}. + * This method is the opposite of {@link attachEventHandler}. It will detach + * any 'on' events definedb by an objects active behaviors as well. * @param string event name * @param callback the event handler to be removed + * @param numeric|false|null the priority of the handler, defaults to false which translates + * to an item of any priority within {@link TPriorityList}; null means the default priority * @return boolean if the removal is successful */ - public function detachEventHandler($name,$handler) + public function detachEventHandler($name,$handler,$priority=false) { if($this->hasEventHandler($name)) { try { - $this->getEventHandlers($name)->remove($handler); + $this->getEventHandlers($name)->remove($handler,$priority); return true; } catch(Exception $e) @@ -403,42 +1037,133 @@ class TComponent } /** - * Raises an event. + * Raises an event. This raises both inter-object 'on' events and global 'fx' events. * This method represents the happening of an event and will - * invoke all attached event handlers for the event. + * invoke all attached event handlers for the event in {@link TPriorityList} order. + * This method does not handle intra-object/behavior dynamic 'dy' events. + * + * There are ways to handle event responses. By defailt {@link EVENT_RESULT_FILTER}, + * all event responses are stored in an array, filtered for null responses, and returned. + * If {@link EVENT_RESULT_ALL} is specified, all returned results will be stored along + * with the sender and param in an array + * <code> + * $result[] = array('sender'=>$sender,'param'=>$param,'response'=>$response); + * </code> + * + * If {@link EVENT_RESULT_FEED_FORWARD} is specified, then each handler result is then + * fed forward as the parameters for the next event. This allows for events to filter data + * directly by affecting the event parameters + * + * If a callable function is set in the response type or the post function filter is specified then the + * result of each called event handler is post processed by the callable function. Used in + * combination with {@link EVENT_RESULT_FEED_FORWARD}, any event (and its result) can be chained. + * + * When raising a global 'fx' event, registered handlers in the global event list for + * {@link GLOBAL_RAISE_EVENT_LISTENER} are always added into the set of event handlers. In this way, + * these global events are always raised for every global 'fx' event. The registered handlers for global + * raiseEvent events have priorities. Any registered global raiseEvent event handlers with a priority less than zero + * are added before the main event handlers being raised and any registered global raiseEvent event handlers + * with a priority equal or greater than zero are added after the main event handlers being raised. In this way + * all {@link GLOBAL_RAISE_EVENT_LISTENER} handlers are always called for every raised 'fx' event. + * + * Behaviors may implement the following functions: + * <code> + * public function dyPreRaiseEvent($name,$sender,$param,$responsetype,$postfunction[, $chain]) { + * return $name; //eg, the event name may be filtered/changed + * } + * public function dyIntraRaiseEventTestHandler($handler,$sender,$param,$name[, $chain]) { + * return true; //should this particular handler be executed? true/false + * } + * public function dyIntraRaiseEventPostHandler($name,$sender,$param,$handler,$response[, $chain]) { + * //contains the per handler response + * } + * public function dyPostRaiseEvent($responses,$name,$sender,$param,$responsetype,$postfunction[, $chain]) { + * return $responses; + * } + * </code> + * to be executed when raiseEvent is called. The 'intra' dynamic events are called per handler in + * the handler loop. + * + * dyPreRaiseEvent has the effect of being able to change the event being raised. This intra + * object/behavior event returns the name of the desired event to be raised. It will pass through + * if no dynamic event is specified, or if the original event name is returned. + * dyIntraRaiseEventTestHandler returns true or false as to whether a specific handler should be + * called for a specific raised event (and associated event arguments) + * dyIntraRaiseEventPostHandler does not return anything. This allows behaviors to access the results + * of an event handler in the per handler loop. + * dyPostRaiseEvent returns the responses. This allows for any post processing of the event + * results from the sum of all event handlers + * + * When handling a catch-all {@link __dycall}, the method name is the name of the event + * and the parameters are the sender, the param, and then the name of the event. + * * @param string the event name * @param mixed the event sender object * @param TEventParameter the event parameter + * @param numeric how the results of the event are tabulated. default: {@link EVENT_RESULT_FILTER} The default filters out + * null responses. optional + * @param function any per handler filtering of the response result needed is passed through + * this if not null. default: null. optional + * @return mixed the results of the event * @throws TInvalidOperationException if the event is undefined * @throws TInvalidDataValueException If an event handler is invalid */ - public function raiseEvent($name,$sender,$param) + public function raiseEvent($name,$sender,$param,$responsetype=null,$postfunction=null) { + $p=$param; + if(is_callable($responsetype)) + { + $postfunction=$responsetype; + $responsetype=null; + } + + if($responsetype===null) + $responsetype=TEventResults::EVENT_RESULT_FILTER; + $name=strtolower($name); - if(isset($this->_e[$name])) + $responses=array(); + + $name=$this->dyPreRaiseEvent($name,$sender,$param,$responsetype,$postfunction); + + if($this->hasEventHandler($name)||$this->hasEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER)) { - foreach($this->_e[$name] as $handler) + $handlers=$this->getEventHandlers($name); + $handlerArray=$handlers->toArray(); + if(strncasecmp($name,'fx',2)===0&&$this->hasEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER)) + { + $globalhandlers=$this->getEventHandlers(TComponent::GLOBAL_RAISE_EVENT_LISTENER); + $handlerArray=array_merge($globalhandlers->toArrayBelowPriority(0),$handlerArray,$globalhandlers->toArrayAbovePriority(0)); + } + $response=null; + foreach($handlerArray as $handler) { + if($this->dyIntraRaiseEventTestHandler($handler,$sender,$param,$name)===false) + continue; + if(is_string($handler)) { if(($pos=strrpos($handler,'.'))!==false) { $object=$this->getSubProperty(substr($handler,0,$pos)); $method=substr($handler,$pos+1); - if(method_exists($object,$method)) - $object->$method($sender,$param); + if(method_exists($object,$method)||strncasecmp($method,'dy',2)===0||strncasecmp($method,'fx',2)===0) + { + if($method=='__dycall') + $response=$object->__dycall($name,array($sender,$param,$name)); + else + $response=$object->$method($sender,$param,$name); + } else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler); } else - call_user_func($handler,$sender,$param); + $response=call_user_func($handler,$sender,$param,$name); } else if(is_callable($handler,true)) { - // an array: 0 - object, 1 - method name/path list($object,$method)=$handler; - if(is_string($object)) // static method call - call_user_func($handler,$sender,$param); + if(is_string($object)) + $response=call_user_func($handler,$sender,$param,$name); else { if(($pos=strrpos($method,'.'))!==false) @@ -446,27 +1171,66 @@ class TComponent $object=$this->getSubProperty(substr($method,0,$pos)); $method=substr($method,$pos+1); } - if(method_exists($object,$method)) - $object->$method($sender,$param); + if(method_exists($object,$method)||strncasecmp($method,'dy',2)===0||strncasecmp($method,'fx',2)===0) + { + if($method=='__dycall') + $response=$object->__dycall($name,array($sender,$param,$name)); + else + $response=$object->$method($sender,$param,$name); + } else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler[1]); } } else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,gettype($handler)); + + $this->dyIntraRaiseEventPostHandler($name,$sender,$param,$handler,$response); + + if($postfunction) + $response=call_user_func_array($postfunction,array($sender,$param,$this,$response)); + + if($responsetype&TEventResults::EVENT_RESULT_ALL) + $responses[]=array('sender'=>$sender,'param'=>$param,'response'=>$response); + else + $responses[]=$response; + + if($response!==null&&($responsetype&TEventResults::EVENT_RESULT_FEED_FORWARD)) + $param=$response; + } } - else if(!$this->hasEvent($name)) + else if(strncasecmp($name,'on',2)===0&&!$this->hasEvent($name)) throw new TInvalidOperationException('component_event_undefined',get_class($this),$name); + + if($responsetype&TEventResults::EVENT_RESULT_FILTER) + $responses=array_filter($responses); + + $responses=$this->dyPostRaiseEvent($responses,$name,$sender,$param,$responsetype,$postfunction); + + return $responses; } /** * Evaluates a PHP expression in the context of this control. + * + * Behaviors may implement the function: + * <code> + * public function dyEvaluateExpressionFilter($expression, $chain) { + * return $chain->dyEvaluateExpressionFilter(str_replace('foo', 'bar', $expression)); //example + * } + * </code> + * to be executed when evaluateExpression is called. All attached behaviors are notified through + * dyEvaluateExpressionFilter. The chaining is important in this function due to the filtering + * pass-through effect. + * + * @param string PHP expression * @return mixed the expression result * @throws TInvalidOperationException if the expression is invalid */ public function evaluateExpression($expression) { + $expression=$this->dyEvaluateExpressionFilter($expression); try { if(eval("\$result=$expression;")===false) @@ -481,12 +1245,24 @@ class TComponent /** * Evaluates a list of PHP statements. + * + * Behaviors may implement the function: + * <code> + * public function dyEvaluateStatementsFilter($statements, $chain) { + * return $chain->dyEvaluateStatementsFilter(str_replace('foo', 'bar', $statements)); //example + * } + * </code> + * to be executed when evaluateStatements is called. All attached behaviors are notified through + * dyEvaluateStatementsFilter. The chaining is important in this function due to the filtering + * pass-through effect. + * * @param string PHP statements * @return string content echoed or printed by the PHP statements * @throws TInvalidOperationException if the statements are invalid */ public function evaluateStatements($statements) { + $statements=$this->dyEvaluateStatementsFilter($statements); try { ob_start(); @@ -508,11 +1284,22 @@ class TComponent * The default implementation of this method will invoke * the potential parent component's {@link addParsedObject}. * This method can be overridden. + * + * Behaviors may implement the function: + * <code> + * public function dyCreatedOnTemplate($parent, $chain) { + * return $chain->dyCreatedOnTemplate($parent); //example + * } + * </code> + * to be executed when createdOnTemplate is called. All attached behaviors are notified through + * dyCreatedOnTemplate. + * * @param TComponent potential parent of this control * @see addParsedObject */ public function createdOnTemplate($parent) { + $parent=$this->dyCreatedOnTemplate($parent); $parent->addParsedObject($this); } @@ -521,11 +1308,381 @@ class TComponent * The object can be either a component or a static text string. * This method can be overridden to customize the handling of newly created objects in template. * Only framework developers and control developers should use this method. + * + * Behaviors may implement the function: + * <code> + * public function dyAddParsedObject($object[, $chain]) { + * } + * </code> + * to be executed when addParsedObject is called. All attached behaviors are notified through + * dyAddParsedObject. + * * @param string|TComponent text string or component parsed and instantiated in template * @see createdOnTemplate */ public function addParsedObject($object) { + $this->dyAddParsedObject($object); + } + + + /** + * This is the method registered for all instanced objects should a class behavior be added after + * the class is instanced. Only when the class to which the behavior is being added is in this + * object's class hierarchy, via {@link getClassHierarchy}, is the behavior added to this instance. + * @param $sender the application + * @param $param TClassBehaviorEventParameter + */ + public function fxAttachClassBehavior($sender,$param) { + if(in_array($param->getClass(),$this->getClassHierarchy(true))) + return $this->attachBehavior($param->getName(),$param->getBehavior(),$param->getPriority()); + } + + + /** + * This is the method registered for all instanced objects should a class behavior be removed after + * the class is instanced. Only when the class to which the behavior is being added is in this + * object's class hierarchy, via {@link getClassHierarchy}, is the behavior removed from this instance. + * @param $sender the application + * @param $param TClassBehaviorEventParameter + */ + public function fxDetachClassBehavior($sender,$param) { + if(in_array($param->getClass(),$this->getClassHierarchy(true))) + return $this->detachBehavior($param->getName(),$param->getPriority()); + } + + + /** + * This will add a class behavior to all classes instanced (that are listening) and future newly instanced objects. + * This registers the behavior for future instances and pushes the changes to all the instances that are listening as well. + * The universal class behaviors are stored in an inverted stack with the latest class behavior being at the first position in the array. + * This is done so class behaviors are added last first. + * @param string name the key of the class behavior + * @param object|string class behavior or name of the object behavior per instance + * @param string|class string of class or class on which to attach this behavior. Defaults to null which will error + * but more important, if this is on PHP 5.3 it will use Late Static Binding to derive the class + * it should extend. + * <code> + * TPanel::attachClassBehavior('javascripts', (new TJsPanelBehavior())->init($this)); + * </code> + * @param numeric|null priority of behavior, default: null the default priority of the {@link TPriorityList} Optional. + * @throws TInvalidOperationException if the class behavior is being added to a {@link TComponent}; due to recursion. + * @throws TInvalidOperationException if the class behavior is already defined + */ + public static function attachClassBehavior($name,$behavior,$class=null,$priority=null) { + if(!$class&&function_exists('get_called_class')) + $class=get_called_class(); + if(!$class) + throw new TInvalidOperationException('component_no_class_provided_nor_late_binding'); + + if(!is_string($name)) + $name=get_class($name); + $class=strtolower($class); + if($class==='tcomponent') + throw new TInvalidOperationException('component_no_tcomponent_class_behaviors'); + if(empty(self::$_um[$class])) + self::$_um[$class]=array(); + if(isset(self::$_um[$class][$name])) + throw new TInvalidOperationException('component_class_behavior_defined',$class,$name); + $param=new TClassBehaviorEventParameter($class,$name,$behavior,$priority); + self::$_um[$class]=array($name=>$param)+self::$_um[$class]; + $behaviorObject=is_string($behavior)?new $behavior:$behavior; + return $behaviorObject->raiseEvent('fxAttachClassBehavior',null,$param); + } + + + /** + * This will remove a behavior from a class. It unregisters it from future instances and + * pulls the changes from all the instances that are listening as well. + * PHP 5.3 uses Late Static Binding to derive the static class upon which this method is called. + * @param $name the key of the class behavior + * @param $class string class on which to attach this behavior. Defaults to null. + * @param $priority numeric|null|false priority. false is any priority, null is default + * {@link TPriorityList} priority, and numeric is a specific priority. + * @throws Exception if the the class cannot be derived from Late Static Binding and is not + * not supplied as a parameter. + */ + public static function detachClassBehavior($name,$class=null,$priority=false) { + if(!$class&&function_exists('get_called_class')) + $class=get_called_class(); + if(!$class) + throw new TInvalidOperationException('component_no_class_provided_nor_late_binding'); + + $class=strtolower($class); + if(!is_string($name)) + $name=get_class($name); + if(empty(self::$_um[$class])||!isset(self::$_um[$class][$name])) + return false; + $param=self::$_um[$class][$name]; + $behavior=$param->getBehavior(); + unset(self::$_um[$class][$name]); + $behaviorObject=is_string($behavior)?new $behavior:$behavior; + return $behaviorObject->raiseEvent('fxDetachClassBehavior',null,$param); + } + + /** + * Returns the named behavior object. + * The name 'asa' stands for 'as a'. + * @param string the behavior name + * @return IBehavior the behavior object, or null if the behavior does not exist + */ + public function asa($behaviorname) + { + return isset($this->_m[$behaviorname])?$this->_m[$behaviorname]:null; + } + + /** + * Returns whether or not the object or any of the behaviors are of a particular class. + * The name 'isa' stands for 'is a'. This first checks if $this is an instanceof the class. + * It then checks each Behavior. If a behavior implements {@link IInstanceCheck}, + * then the behavior can determine what it is an instanceof. If this behavior function returns true, + * then this method returns true. If the behavior instance checking function returns false, + * then no further checking is performed as it is assumed to be correct. + * + * If the behavior instance check function returns nothing or null or the behavior + * doesn't implement the {@link IInstanceCheck} interface, then the default instanceof occurs. + * The default isa behavior is to check if the behavior is an instanceof the class. + * + * The behavior {@link IInstanceCheck} is to allow a behavior to have the host object + * act as a completely different object. + * + * @param class or string + * @return boolean whether or not the object or a behavior is an instance of a particular class + */ + public function isa($class) + { + if($this instanceof $class) + return true; + if($this->_m!==null&&$this->_behaviorsenabled) + foreach($this->_m->toArray() as $behavior){ + if(($behavior instanceof IBehavior)&&!$behavior->getEnabled()) + continue; + + $check = null; + if(($behavior->isa('IInstanceCheck'))&&$check=$behavior->isinstanceof($class,$this)) + return true; + if($check===null&&($behavior->isa($class))) + return true; + } + return false; + } + + /** + * Attaches a list of behaviors to the component. + * Each behavior is indexed by its name and should be an instance of + * {@link IBehavior}, a string specifying the behavior class, or a + * {@link TClassBehaviorEventParameter}. + * @param array list of behaviors to be attached to the component + */ + public function attachBehaviors($behaviors) + { + foreach($behaviors as $name=>$behavior) + if($behavior instanceof TClassBehaviorEventParameter) + $this->attachBehavior($behavior->getName(),$behavior->getBehavior(),$behavior->getPriority()); + else + $this->attachBehavior($name,$behavior); + } + + /** + * Detaches select behaviors from the component. + * Each behavior is indexed by its name and should be an instance of + * {@link IBehavior}, a string specifying the behavior class, or a + * {@link TClassBehaviorEventParameter}. + * @param array list of behaviors to be detached from the component + */ + public function detachBehaviors($behaviors) + { + if($this->_m!==null) + { + foreach($behaviors as $name=>$behavior) + if($behavior instanceof TClassBehaviorEventParameter) + $this->detachBehavior($behavior->getName(),$behavior->getPriority()); + else + $this->detachBehavior(is_string($behavior)?$behavior:$name); + } + } + + /** + * Detaches all behaviors from the component. + */ + public function clearBehaviors() + { + if($this->_m!==null) + { + foreach($this->_m->toArray() as $name=>$behavior) + $this->detachBehavior($name); + $this->_m=null; + } + } + + /** + * Attaches a behavior to this component. + * This method will create the behavior object based on the given + * configuration. After that, the behavior object will be initialized + * by calling its {@link IBehavior::attach} method. + * + * Already attached behaviors may implement the function: + * <code> + * public function dyAttachBehavior($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when attachBehavior is called. All attached behaviors are notified through + * dyAttachBehavior. + * + * @param string the behavior's name. It should uniquely identify this behavior. + * @param mixed the behavior configuration. This is passed as the first + * parameter to {@link YiiBase::createComponent} to create the behavior object. + * @return IBehavior the behavior object + */ + public function attachBehavior($name,$behavior,$priority=null) + { + if(is_string($behavior)) + $behavior=Prado::createComponent($behavior); + if(!($behavior instanceof IBaseBehavior)) + throw new TInvalidDataTypeException('component_not_a_behavior',get_class($behavior)); + if($behavior instanceof IBehavior) + $behavior->setEnabled(true); + if($this->_m===null) + $this->_m=new TPriorityMap; + $behavior->attach($this); + $this->dyAttachBehavior($name,$behavior); + $this->_m->add($name,$behavior,$priority); + return $behavior; + } + + /** + * Detaches a behavior from the component. + * The behavior's {@link IBehavior::detach} method will be invoked. + * + * Behaviors may implement the function: + * <code> + * public function dyDetachBehavior($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when detachBehavior is called. All attached behaviors are notified through + * dyDetachBehavior. + * + * @param string the behavior's name. It uniquely identifies the behavior. + * @param numeric the behavior's priority. This defaults to false, aka any priority. + * @return IBehavior the detached behavior. Null if the behavior does not exist. + */ + public function detachBehavior($name,$priority=false) + { + if($this->_m!=null&&isset($this->_m[$name])) + { + $this->_m[$name]->detach($this); + $behavior=$this->_m->itemAt($name); + $this->_m->remove($name,$priority); + $this->dyDetachBehavior($name,$behavior); + return $behavior; + } + } + + /** + * Enables all behaviors attached to this component independent of the behaviors + * + * Behaviors may implement the function: + * <code> + * public function dyEnableBehaviors($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when enableBehaviors is called. All attached behaviors are notified through + * dyEnableBehaviors. + * + */ + public function enableBehaviors() + { + if(!$this->_behaviorsenabled) + { + $this->_behaviorsenabled=true; + $this->dyEnableBehaviors(); + } + } + + /** + * Disables all behaviors attached to this component independent of the behaviors + * + * Behaviors may implement the function: + * <code> + * public function dyDisableBehaviors($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when disableBehaviors is called. All attached behaviors are notified through + * dyDisableBehaviors. + * + */ + public function disableBehaviors() + { + if($this->_behaviorsenabled) + { + $this->dyDisableBehaviors(); + $this->_behaviorsenabled=false; + } + } + + + /** + * Returns if all the behaviors are turned on or off for the object. + * @return boolean whether or not all behaviors are enabled (true) or not (false) + */ + public function getBehaviorsEnabled() + { + return $this->_behaviorsenabled; + } + + /** + * Enables an attached object behavior. This cannot enable or disable whole class behaviors. + * A behavior is only effective when it is enabled. + * A behavior is enabled when first attached. + * + * Behaviors may implement the function: + * <code> + * public function dyEnableBehavior($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when enableBehavior is called. All attached behaviors are notified through + * dyEnableBehavior. + * + * @param string the behavior's name. It uniquely identifies the behavior. + */ + public function enableBehavior($name) + { + if($this->_m!=null&&isset($this->_m[$name])){ + if($this->_m[$name] instanceof IBehavior) { + $this->_m[$name]->setEnabled(true); + $this->dyEnableBehavior($name,$this->_m[$name]); + return true; + } + return false; + } + return null; + } + + /** + * Disables an attached behavior. This cannot enable or disable whole class behaviors. + * A behavior is only effective when it is enabled. + * + * Behaviors may implement the function: + * <code> + * public function dyDisableBehavior($name,$behavior[, $chain]) { + * } + * </code> + * to be executed when disableBehavior is called. All attached behaviors are notified through + * dyDisableBehavior. + * + * @param string the behavior's name. It uniquely identifies the behavior. + */ + public function disableBehavior($name) + { + if($this->_m!=null&&isset($this->_m[$name])){ + if($this->_m[$name] instanceof IBehavior) { + $this->_m[$name]->setEnabled(false); + $this->dyDisableBehavior($name,$this->_m[$name]); + return true; + } + return false; + } + return null; } /** @@ -554,6 +1711,91 @@ class TComponent } } + +/** + * IDynamicMethods interface. + * IDynamicMethods marks an object to receive undefined global or dynamic events. + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +interface IDynamicMethods +{ + public function __dycall($method,$args); +} + + + +/** + * TClassBehaviorEventParameter class. + * TClassBehaviorEventParameter is the parameter sent with the class behavior changes. + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +class TClassBehaviorEventParameter extends TEventParameter +{ + private $_class; + private $_name; + private $_behavior; + private $_priority; + + /** + * Holds the parameters for the Class Behavior Events + * @param string $class this is the class to get the behavior + * @param string $name the name of the behavior + * @param object $behavior this is the behavior to implement the class behavior + */ + public function __construct($class,$name,$behavior,$priority) + { + $this->_class=$class; + $this->_name=$name; + $this->_behavior=$behavior; + $this->_priority=$priority; + } + + /** + * This is the class to get the behavior + * @return string the class to get the behavior + */ + public function getClass() + { + return $this->_class; + } + + /** + * name of the behavior + * @return string the name to get the behavior + */ + public function getName() + { + return $this->_name; + } + + /** + * This is the behavior which the class is to get + * @return object the behavior to implement + */ + public function getBehavior() + { + return $this->_behavior; + } + + /** + * This is the priority which the behavior is to get + * @return numeric the priority of the behavior + */ + public function getPriority() + { + return $this->_priority; + } +} + + /** * TEnumerable class. * TEnumerable is the base class for all enumerable types. @@ -578,11 +1820,11 @@ class TComponent */ class TEnumerable implements Iterator { - private $_enums = array(); + private $_enums=array(); public function __construct() { - $reflection = new ReflectionClass($this); - $this->_enums = $reflection->getConstants(); + $reflection=new ReflectionClass($this); + $this->_enums=$reflection->getConstants(); } public function current() { @@ -602,7 +1844,7 @@ class TEnumerable implements Iterator } public function valid() { - return $this->current() !== false; + return $this->current()!==false; } } @@ -779,7 +2021,7 @@ class TPropertyValue */ public static function ensureNullIfEmpty($value) { - return empty($value) ? null : $value; + return empty($value)?null:$value; } } @@ -796,6 +2038,12 @@ class TEventParameter extends TComponent { } +class TEventResults extends TEnumerable { + const EVENT_RESULT_FEED_FORWARD=1; + const EVENT_RESULT_FILTER=2; + const EVENT_RESULT_ALL=4; +} + /** * TComponentReflection class. * @@ -978,6 +2226,117 @@ class TComponentReflection extends TComponent } } + + + + +/** + * IBaseBehavior interface is the base behavior class from which all other + * behaviors types are derived + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +interface IBaseBehavior { + /** + * Attaches the behavior object to the component. + * @param CComponent the component that this behavior is to be attached to. + */ + public function attach($component); + /** + * Detaches the behavior object from the component. + * @param CComponent the component that this behavior is to be detached from. + */ + public function detach($component); +} + +/** + * IBehavior interfaces is implemented by instance behavior classes. + * + * A behavior is a way to enhance a component with additional methods and + * events that are defined in the behavior class and not available in the + * class. Objects may signal behaviors through dynamic events. + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +interface IBehavior extends IBaseBehavior +{ + /** + * @return boolean whether this behavior is enabled + */ + public function getEnabled(); + /** + * @param boolean whether this behavior is enabled + */ + public function setEnabled($value); +} + + +/** + * IClassBehavior interface is implements behaviors across all instances of + * a particular class + * + * Any calls to functions not present in the original object but to behaviors + * derived from this class, will have inserted as the first argument parameter + * the object containing the behavior. + * + * For example: + * <code> + * $objWithClassBehavior->MethodOfClassBehavior(1, 20); + * </code> + * will be acted within the class behavior like this: + * <code> + * public function MethodOfClassBehavior($object, $firstParam, $secondParam){ + * // $object === $objWithClassBehavior, $firstParam === 1, $secondParam === 20 + * } + * </code> + * + * This also holds for 'dy' events as well. For dynamic events, method arguments would be: + * <code> + * public function dyMethodOfClassBehavior($object, $firstParam, $secondParam, $callchain){ + * // $object === $objWithClassBehavior, $firstParam === 1, $secondParam === 20, $callchain instanceof {@link TCallChain} + * } + * </code> + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +interface IClassBehavior extends IBaseBehavior { +} + + +/** + * IInstanceCheck This interface allows objects to determine their own + * 'instanceof' results when {@link TComponent::isa} is called. This is + * important with behaviors because behaviors may want to look like + * particular objects other than themselves. + * + * @author Brad Anderson <javalizard@mac.com> + * @version $Id$ + * @package System + * @since 3.2 + */ +interface IInstanceCheck { + /** + * The method checks $this or, if needed, the parameter $instance is of type + * class. In the case of a Class Behavior, the instance to which the behavior + * is attached may be important to determine if $this is an instance + * of a particular class. + * @param class|string the component that this behavior is checking if it is an instanceof. + * @param object the object which the behavior is attached to. default: null + * @return boolean|null if the this or the instance is of type class. When null, no information could be derived and + * the default mechanisms take over. + */ + public function isinstanceof($class,$instance=null); +} + /** * TJavaScriptLiteral class that encloses string literals that are not * supposed to be escaped by {@link TJavaScript::encode() } @@ -1035,3 +2394,4 @@ class TJavaScriptString extends TJavaScriptLiteral return TJavaScript::jsonEncode((string)$this->_s,JSON_HEX_QUOT | JSON_HEX_APOS | JSON_HEX_TAG); } } + diff --git a/framework/Util/TBehavior.php b/framework/Util/TBehavior.php new file mode 100644 index 00000000..465f9673 --- /dev/null +++ b/framework/Util/TBehavior.php @@ -0,0 +1,87 @@ +<?php +/** + * CBehavior class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CBehavior is a convenient base class for behavior classes. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CBehavior.php 564 2009-01-21 22:07:10Z qiang.xue $ + * @package system.base + * @since 1.0.2 + */ +class TBehavior extends TComponent implements IBehavior +{ + private $_enabled; + private $_owner; + + /** + * Declares events and the corresponding event handler methods. + * The events are defined by the {@link owner} component, while the handler + * methods by the behavior class. The handlers will be attached to the corresponding + * events when the behavior is attached to the {@link owner} component; and they + * will be detached from the events when the behavior is detached from the component. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array(); + } + + /** + * Attaches the behavior object to the component. + * The default implementation will set the {@link owner} property + * and attach event handlers as declared in {@link events}. + * Make sure you call the parent implementation if you override this method. + * @param TComponent the component that this behavior is to be attached to. + */ + public function attach($owner) + { + $this->_owner=$owner; + foreach($this->events() as $event=>$handler) + $owner->attachEventHandler($event,array($this,$handler)); + } + + /** + * Detaches the behavior object from the component. + * The default implementation will unset the {@link owner} property + * and detach event handlers declared in {@link events}. + * Make sure you call the parent implementation if you override this method. + * @param TComponent the component that this behavior is to be detached from. + */ + public function detach($owner) + { + foreach($this->events() as $event=>$handler) + $owner->detachEventHandler($event,array($this,$handler)); + $this->_owner=null; + } + + /** + * @return CComponent the owner component that this behavior is attached to. + */ + public function getOwner() + { + return $this->_owner; + } + + /** + * @return boolean whether this behavior is enabled + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * @param boolean whether this behavior is enabled + */ + public function setEnabled($value) + { + $this->_enabled=$value; + } +}
\ No newline at end of file diff --git a/framework/Util/TCallChain.php b/framework/Util/TCallChain.php new file mode 100644 index 00000000..ae6e02fc --- /dev/null +++ b/framework/Util/TCallChain.php @@ -0,0 +1,147 @@ +<?php +/** + * TCallChain class file. + * + * @author Brad Anderson <javalizard@gmail.com> + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2008-2011 Pradosoft + * @license http://www.pradosoft.com/license/ + */ + +/** + * TCallChain is a recursive event calling mechanism. This class implements + * the {@link IDynamicMethods} class so that any 'dy' event calls can be caught + * and patched through to the intended recipient + * @author Brad Anderson <javalizard@gmail.com> + * @version $Id: TCallChain.php 564 2009-01-21 22:07:10Z javalizard $ + * @package system.base + * @since 3.2 + */ +class TCallChain extends TList implements IDynamicMethods +{ + /** + * @var {@link TListIterator} for moving through the chained method calls + */ + private $_iterator=null; + + /** + * @var string the method name of the call chain + */ + private $_method=null; + + /** + * This initializes the list and the name of the method to be called + * @param string the name of the function call + */ + public function __construct($method) { + $this->_method=$method; + parent::__construct(); + } + + + /** + * This initializes the list and the name of the method to be called + * @param string|array this is a callable function as a string or array with + * the object and method name as string + * @param array The array of arguments to the function call chain + */ + public function addCall($method,$args) + { + $this->add(array($method,$args)); + } + + /** + * This method calls the next Callable in the list. All of the method arguments + * coming into this method are substituted into the original method argument of + * call in the chain. + * + * If the original method call has these parameters + * <code> + * $originalobject->dyExampleMethod('param1', 'param2', 'param3') + * </code> + * <code> + * $callchain->dyExampleMethod('alt1', 'alt2') + * </code> + * then the next call in the call chain will recieve the parameters as if this were called + * <code> + * $behavior->dyExampleMethod('alt1', 'alt2', 'param3', $callchainobject) + * </code> + * + * When dealing with {@link IClassBehaviors}, the first parameter of the stored argument + * list in 'dy' event calls is always the object containing the behavior. This modifies + * the parameter replacement mechanism slightly to leave the object containing the behavior + * alone and only replacing the other parameters in the argument list. As per {@link __call}, + * any calls to a 'dy' event do not need the object containing the behavior as the addition of + * the object to the argument list as the first element is automatic for IClassBehaviors. + * + * The last parameter of the method parameter list for any callable in the call chain + * will be the TCallChain object itself. This is so that any behavior implementing + * these calls will have access to the call chain. Each callable should either call + * the TCallChain call method internally for direct chaining or call the method being + * chained (in which case the dynamic handler will pass through to this call method). + * + * If the dynamic intra object/behavior event is not called in the behavior implemented + * dynamic method, it will return to this method and call the following behavior + * implementation so as no behavior with an implementation of the dynamic event is left + * uncalled. This does break the call chain though and will not act as a "parameter filter". + * + * When there are no handlers or no handlers left, it returns the first parameter of the + * argument list. + * + */ + public function call() + { + $args=func_get_args(); + if($this->getCount()===0) + return isset($args[0])?$args[0]:null; + + if(!$this->_iterator) + { + $chain_array=array_reverse($this->toArray()); + $this->_iterator=new TListIterator($chain_array); + } + if($this->_iterator->valid()) + do { + $handler=$this->_iterator->current(); + $this->_iterator->next(); + if(is_array($handler[0])&&$handler[0][0] instanceof IClassBehavior) + array_splice($handler[1],1,count($args),$args); + else + array_splice($handler[1],0,count($args),$args); + $handler[1][]=$this; + $result=call_user_func_array($handler[0],$handler[1]); + } while($this->_iterator->valid()); + else + $result = $args[0]; + return $result; + } + + + /** + * This catches all the unpatched dynamic events. When the method call matches the + * call chain method, it passes the arguments to the original __call (of the dynamic + * event being unspecified in TCallChain) and funnels into the method {@link call}, + * so the next dynamic event handler can be called. + * If the original method call has these parameters + * <code> + * $originalobject->dyExampleMethod('param1', 'param2', 'param3') + * </code> + * and within the chained dynamic events, this can be called + * <code> + * class DyBehavior extends TBehavior { + * public function dyExampleMethod($param1, $param2, $param3, $callchain) + * $callchain->dyExampleMethod($param1, $param2, $param3) + * } + * { + * </code> + * to call the next event in the chain. + * @param string method name of the unspecified object method + * @param array arguments to the unspecified object method + */ + public function __dycall($method,$args) + { + if($this->_method==$method) + return call_user_func_array(array($this,'call'),$args); + return null; + } +}
\ No newline at end of file diff --git a/framework/Util/TClassBehavior.php b/framework/Util/TClassBehavior.php new file mode 100644 index 00000000..807549b7 --- /dev/null +++ b/framework/Util/TClassBehavior.php @@ -0,0 +1,36 @@ +<?php +/** + * TClassBehavior class file. + * + * @author Brad Anderson <javalizard@gmail.com> + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2008-2011 Pradosoft + * @license http://www.pradosoft.com/license/ + */ + +/** + * TClassBehavior is a convenient base class for whole class behaviors. + * @author Brad Anderson <javalizard@gmail.com> + * @version $Id: TClassBehavior.php 564 2009-01-21 22:07:10Z javalizard $ + * @package system.base + * @since 3.1.8 + */ +class TClassBehavior extends TComponent implements IClassBehavior +{ + + /** + * Attaches the behavior object to the component. + * @param TComponent the component that this behavior is to be attached to. + */ + public function attach($component) + { + } + + /** + * Detaches the behavior object from the component. + * @param TComponent the component that this behavior is to be detached from. + */ + public function detach($component) + { + } +}
\ No newline at end of file diff --git a/tests/unit/TComponentTest.php b/tests/unit/TComponentTest.php index 25292c0d..7722f9d6 100644 --- a/tests/unit/TComponentTest.php +++ b/tests/unit/TComponentTest.php @@ -1,11 +1,21 @@ <?php require_once dirname(__FILE__).'/phpunit.php'; +Prado::using('System.Util.*'); +Prado::using('System.Collections.TPriorityList'); +Prado::using('System.Collections.TPriorityMap'); + class NewComponent extends TComponent { private $_object = null; private $_text = 'default'; private $_eventHandled = false; + private $_return = null; + private $_colorattribute = null; + public function getAutoGlobalListen() { + return true; + } + public function getText() { return $this->_text; } @@ -14,6 +24,14 @@ class NewComponent extends TComponent { $this->_text=$value; } + public function getReadOnlyProperty() { + return 'read only'; + } + + public function getJsReadOnlyJsProperty() { + return 'js read only'; + } + public function getObject() { if(!$this->_object) { $this->_object=new NewComponent; @@ -30,10 +48,363 @@ class NewComponent extends TComponent { $this->_eventHandled=true; } + public function eventReturnValue($sender,$param) { + return $param->Return; + } + public function isEventHandled() { return $this->_eventHandled; } + + public function resetEventHandled() { + $this->_eventHandled = false; + } + public function getReturn() { + return $this->_return; + } + public function setReturn($return) { + $this->_return = $return; + } + public function getjsColorAttribute() { + return $this->_colorattribute; + } + public function setjsColorAttribute($colorattribute) { + $this->_colorattribute = $colorattribute; + } +} + + +class NewComponentNoListen extends NewComponent { + // this object does _not_ auto install global listeners during construction + public function getAutoGlobalListen() { + return false; + } +} + +class DynamicCatchingComponent extends NewComponentNoListen implements IDynamicMethods { + public function __dycall($method, $args) { + + } +} + + +class GlobalRaiseComponent extends NewComponent implements IDynamicMethods { + private $_callorder = array(); + + public function getCallOrders() + { + return $this->_callorder; + } + public function __dycall($method, $args) { + if(strncasecmp($method,'fx',2)!==0) return; + $this->_callorder[] = 'fxcall'; + } + public function fxGlobalListener($sender,$param,$name) { + $this->_callorder[] = 'fxGL'; + } + public function fxPrimaryGlobalEvent($sender,$param,$name) { + $this->_callorder[] = 'primary'; + } + public function commonRaiseEventListener($sender,$param,$name) { + $this->_callorder[] = 'com'; + } + public function postglobalRaiseEventListener($sender,$param,$name) { + $this->_callorder[] = 'postgl'; + } + public function preglobalRaiseEventListener($sender,$param,$name) { + $this->_callorder[] = 'pregl'; + } +} + + + +class FooClassBehavior extends TClassBehavior { + private $_baseObject = null; + public function faaEverMore($object, $laa, $sol) { + $this->_baseObject = $object; + return $laa * $sol; + } + public function getLastClassObject() {return $this->_baseObject;} +} + +class FooFooClassBehavior extends FooClassBehavior { + public function faafaaEverMore($object, $laa, $sol) { + + } +} + +class BarClassBehavior extends TClassBehavior { + public function moreFunction($object, $laa, $sol) { + return true; + } +} + + + +class FooBehavior extends TBehavior { + public function faaEverMore($laa, $sol) { + return true; + } } +class FooFooBehavior extends FooBehavior { + + public function faafaaEverMore($laa, $sol) { + return sqrt($laa * $laa + $sol * $sol); + } +} +class FooBarBehavior extends TBehavior { + public function moreFunction($laa, $sol) { + return $laa * $sol * $sol; + } +} + +class PreBarBehavior extends TBehavior { +} + +class BarBehavior extends PreBarBehavior implements IInstanceCheck { + private $_instanceReturn = null; + + public function moreFunction($laa, $sol) { + return pow($laa+$sol+1, 2); + } + + public function isinstanceof($class, $instance=null) { + return $this->_instanceReturn; + } + public function setInstanceReturn($value) { + $this->_instanceReturn = $value; + } +} +class DynamicCallComponent extends NewComponent implements IDynamicMethods { + public function __dycall($method, $args) { + if($method === 'dyPowerFunction') + return pow($args[0], $args[1]); + if($method === 'dyDivisionFunction') + return $args[0] / $args[1]; + if($method === 'fxPowerFunction') + return 2*pow($args[0], $args[1]); + if($method === 'fxDivisionFunction') + return 2*$args[0] / $args[1]; + } +} + + + +class BehaviorTestBehavior extends TBehavior { + private $excitement = 'faa'; + public function getExcitement() { + return $this->excitement; + } + public function setExcitement($value) { + $this->excitement = $value; + } + public function getReadOnly() { + return true; + } + + public function onBehaviorEvent($sender, $param,$responsetype=null,$postfunction=null) { + return $this->getOwner()->raiseEvent('onBehaviorEvent',$sender,$param,$responsetype,$postfunction); + } + public function fxGlobalBehaviorEvent($sender, $param) { + } +} + +class dy1TextReplace extends TBehavior { + protected $_called = false; + public function dyTextFilter($text, $callchain) { + $this->_called = true; + return str_replace("..", '__', $callchain->dyTextFilter($text)); + } + public function isCalled() { + return $this->_called; + } + public function dyPowerFunction($x, $y, $callchain) + { + return pow($x / $callchain->dyPowerFunction($x, $y), $y); + } +} +class dy2TextReplace extends dy1TextReplace { + public function dyTextFilter($text, $callchain) { + $this->_called = true; + return str_replace("++", '||', $callchain->dyTextFilter($text)); + } +} +class dy3TextReplace extends dy1TextReplace { + public function dyTextFilter($text, $callchain) { + $this->_called = true; + return str_replace("!!", '??', $callchain->dyTextFilter($text)); + } +} + +class dy1ClassTextReplace extends TClassBehavior { + protected $_called = false; + public function dyTextFilter($hostobject, $text, $callchain) { + $this->_called = true; + return str_replace("__", '..', $callchain->dyTextFilter($text)); + } + public function isCalled() { + return $this->_called; + } + public function dyPowerFunction($hostobject, $x, $y, $callchain) + { + return pow($x / $callchain->dyPowerFunction($x, $y), $y); + } +} +class dy2ClassTextReplace extends dy1ClassTextReplace { + public function dyTextFilter($hostobject, $text, $callchain) { + $this->_called = true; + return str_replace("||", '++', $callchain->dyTextFilter($text)); + } +} +class dy3ClassTextReplace extends dy1ClassTextReplace { + public function dyTextFilter($hostobject, $text, $callchain) { + $this->_called = true; + return str_replace("??", '^_^', $callchain->dyTextFilter($text)); + } +} + + +class IntraObjectExtenderBehavior extends TBehavior { + + private $lastCall = null; + private $arglist = null; + + public function getLastCall() { + $v = $this->lastCall; + $this->lastCall = null; + return $v; + } + + public function getLastArgumentList() { + $v = $this->arglist; + $this->arglist = null; + return $v; + } + + + + public function dyListen($fx, $chain) { + $this->lastCall = 1; + $this->arglist = func_get_args(); + + return $chain->dyListen($fx); // Calls the next event, within a chain + } + public function dyUnlisten($fx, $chain) { + $this->lastCall = 2; + $this->arglist = func_get_args(); + + return $chain->dyUnlisten($fx); + } + public function dyPreRaiseEvent($name,$sender,$param,$responsetype,$postfunction, $chain) { + $this->lastCall = 3; + $this->arglist = func_get_args(); + + return $chain->dyPreRaiseEvent($name);// Calls the next event, within a chain, if parameters are left off, they are filled in with + // the original parameters passed to the dynamic event. Parameters can be passed if they are changed. + } + public function dyIntraRaiseEventTestHandler($handler,$sender,$param,$name, $chain) { + $this->lastCall += 4; + $this->arglist = func_get_args(); + } + public function dyIntraRaiseEventPostHandler($name,$sender,$param,$handler, $chain) { + $this->lastCall += 5; + $this->arglist = func_get_args(); + } + public function dyPostRaiseEvent($responses,$name,$sender,$param,$responsetype,$postfunction, $chain) { + $this->lastCall += 6; + $this->arglist = func_get_args(); + } + public function dyEvaluateExpressionFilter($expression, $chain) { + $this->lastCall = 7; + $this->arglist = func_get_args(); + return $expression; + } + public function dyEvaluateStatementsFilter($statement, $chain) { + $this->lastCall = 8; + $this->arglist = func_get_args(); + return $statement; + } + public function dyCreatedOnTemplate($parent, $chain) { + $this->lastCall = 9; + $this->arglist = func_get_args(); + return $parent; + } + public function dyAddParsedObject($object, $chain) { + $this->lastCall = 10; + $this->arglist = func_get_args(); + } + public function dyAttachBehavior($name,$behavior, $chain) { + $this->lastCall = 11; + $this->arglist = func_get_args(); + } + public function dyDetachBehavior($name,$behavior, $chain) { + $this->lastCall = 12; + $this->arglist = func_get_args(); + } + public function dyEnableBehaviors($chain) { + $this->lastCall += 13; + $this->arglist = func_get_args(); + } + public function dyDisableBehaviors($chain) { + $this->lastCall = 14; + $this->arglist = func_get_args(); + } + public function dyEnableBehavior($name,$behavior, $chain) { + $this->lastCall = 15; + $this->arglist = func_get_args(); + } + public function dyDisableBehavior($name,$behavior, $chain) { + $this->lastCall = 16; + $this->arglist = func_get_args(); + } +} + + +class IntraClassObjectExtenderBehavior extends TClassBehavior { +} + + +class TDynamicBehavior extends TBehavior implements IDynamicMethods { + private $_dyMethod = null; + public function getLastBehaviorDynamicMethodCalled() { + return $this->_dyMethod; + } + public function __dycall($method, $args) { + $this->_dyMethod = $method; + if($method == 'dyTestDynamicBehaviorMethod') + return $args[0] / $args[1]; + } + public function dyTestIntraEvent($param1, $param2, $chain) { + return $chain->dyTestIntraEvent($param1*2*$param2, $param2); + } + public function TestBehaviorMethod($param1, $param2) { + return $param1 * $param2; + } +} + + +class TDynamicClassBehavior extends TClassBehavior implements IDynamicMethods { + private $_dyMethod = null; + public function getLastBehaviorDynamicMethodCalled() { + return $this->_dyMethod; + } + //Dynamic Calls within class behaviors contain the main object as the first parameter within the args + public function __dycall($method, $args) { + $this->_dyMethod = $method; + $object = array_shift($args); + if($method == 'dyTestDynamicClassBehaviorMethod') + return $args[0] / $args[1]; + } + public function dyTestIntraEvent($object, $param1, $param2, $chain) { + return $chain->dyTestIntraEvent($param1*2*$param2, $param2); + } + public function TestBehaviorMethod($object, $param1, $param2) { + return $param1 * $param2; + } +} + + + /** * @package System @@ -46,26 +417,761 @@ class TComponentTest extends PHPUnit_Framework_TestCase { $this->component = new NewComponent(); } + public function tearDown() { + // PHP version 5.3.6 doesn't call the __destruct method when unsetting variables; + // Thus any object that listens must be explicitly call unlisten in this version of PHP. + if($this->component) + $this->component->unlisten(); $this->component = null; } + + + public function testGetListeningToGlobalEvents() { + $this->assertEquals(true, $this->component->getListeningToGlobalEvents()); + $this->component->unlisten(); + $this->assertEquals(false, $this->component->getListeningToGlobalEvents()); + } + + + public function testConstructorAutoListen() { + // the default object auto installs class behavior hooks + $this->assertEquals(1, $this->component->getEventHandlers('fxAttachClassBehavior')->getCount()); + $this->assertEquals(1, $this->component->getEventHandlers('fxDetachClassBehavior')->getCount()); + $this->assertTrue($this->component->getListeningToGlobalEvents()); + + // this object does not auto install class behavior hooks, thus not changing the global event structure. + // Creating a new instance should _not_ influence the fxAttachClassBehavior and fxDetachClassBehavior + // count. + $component_nolisten = new NewComponentNoListen(); + $this->assertEquals(1, $this->component->getEventHandlers('fxAttachClassBehavior')->getCount()); + $this->assertEquals(1, $this->component->getEventHandlers('fxDetachClassBehavior')->getCount()); + $this->assertEquals(1, $component_nolisten->getEventHandlers('fxAttachClassBehavior')->getCount()); + $this->assertEquals(1, $component_nolisten->getEventHandlers('fxDetachClassBehavior')->getCount()); + + // tests order of class behaviors when a parent and class have class behavior. + // The child should override the parent object-oriented programming style + $this->component->attachClassBehavior('Bar', 'BarBehavior', 'NewComponentNoListen'); + $this->component->attachClassBehavior('FooBar', 'FooBarBehavior', 'NewComponent'); + + //create new object with new class behaviors built in, defined in the two lines above + $component = new NewComponentNoListen; + + $this->assertEquals(25, $component->moreFunction(2, 2)); + + $this->assertEquals(25, $component->Bar->moreFunction(2, 2)); + $this->assertEquals(8, $component->FooBar->moreFunction(2, 2)); + + $component->unlisten();// unwind object and class behaviors + $this->component->detachClassBehavior('FooBar', 'NewComponent'); + $this->component->detachClassBehavior('Bar', 'NewComponentNoListen'); + + } + + + public function testListenAndUnlisten() { + + $component = new NewComponentNoListen(); + + $this->assertEquals(false, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(1, $component->getEventHandlers('fxAttachClassBehavior')->getCount()); + + $this->assertEquals(2, $component->listen()); + + $this->assertEquals(true, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(2, $component->getEventHandlers('fxAttachClassBehavior')->getCount()); + + $this->assertEquals(2, $component->unlisten()); + + $this->assertEquals(false, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(1, $component->getEventHandlers('fxAttachClassBehavior')->getCount()); + } + + + public function testListenAndUnlistenWithDynamicEventCatching() { + + $component = new DynamicCatchingComponent(); + + $this->assertEquals(false, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(0, $component->getEventHandlers(TComponent::GLOBAL_RAISE_EVENT_LISTENER)->getCount()); + + // this adds the fxAttachClassBehavior, fxDetachClassBehavior, and __dycall of the component + $this->assertEquals(3, $component->listen()); + + $this->assertEquals(true, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(1, $component->getEventHandlers(TComponent::GLOBAL_RAISE_EVENT_LISTENER)->getCount()); + + $this->assertEquals(3, $component->unlisten()); + + $this->assertEquals(false, $component->getListeningToGlobalEvents()); + + //This is from $this->component being instanced and listening. $component is accessing the global event structure + $this->assertEquals(0, $component->getEventHandlers(TComponent::GLOBAL_RAISE_EVENT_LISTENER)->getCount()); + } + + + + //Test Class behaviors + public function testAttachClassBehavior() { + + // ensure that the class is listening + $this->assertEquals(1, $this->component->getEventHandlers('fxAttachClassBehavior')->getCount()); + + //Test that the component is not a FooClassBehavior + $this->assertNull($this->component->asa('FooClassBehavior'), "Component is already a FooClassBehavior and should not have this behavior"); + + //Add the FooClassBehavior + $this->component->attachClassBehavior('FooClassBehavior', new FooClassBehavior); + + //Test that the existing listening component can be a FooClassBehavior + $this->assertNotNull($this->component->asa('FooClassBehavior'), "Component is does not have the FooClassBehavior and should have this behavior"); + + // test if the function modifies new instances of the object + $anothercomponent = new NewComponent(); + + //The new component should be a FooClassBehavior + $this->assertNotNull($anothercomponent->asa('FooClassBehavior'), "anothercomponent does not have the FooClassBehavior"); + + // test when overwriting an existing class behavior, it should throw an TInvalidOperationException + try { + $this->component->attachClassBehavior('FooClassBehavior', new BarClassBehavior); + $this->fail('TInvalidOperationException not raised when overwriting an existing behavior'); + } catch(TInvalidOperationException $e) { + } + + + // test when overwriting an existing class behavior, it should throw an TInvalidOperationException + try { + $this->component->attachClassBehavior('FooBarBehavior', 'FooBarBehavior', 'TComponent'); + $this->fail('TInvalidOperationException not raised when trying to place a behavior on the root object TComponent'); + } catch(TInvalidOperationException $e) { + } + + + // test if the function does not modify any existing objects that are not listening + // The FooClassBehavior is already a part of the class behaviors thus the new instance gets the behavior. + $nolistencomponent = new NewComponentNoListen(); + + // test if the function modifies all existing objects that are listening + // Adding a behavior to the first object, the second instance should automatically get the class behavior. + // This is because the second object is listening to the global events of class behaviors + $this->component->attachClassBehavior('BarClassBehavior', new BarClassBehavior); + $this->assertNotNull($anothercomponent->asa('BarClassBehavior'), "anothercomponent is does not have the BarClassBehavior"); + + // The no listen object should not have the BarClassBehavior because it was added as a class behavior after the object was instanced + $this->assertNull($nolistencomponent->asa('BarClassBehavior'), "nolistencomponent has the BarClassBehavior and should not"); + + // But the no listen object should have the FooClassBehavior because the class behavior was installed before the object was instanced + $this->assertNotNull($nolistencomponent->asa('FooClassBehavior'), "nolistencomponent is does not have the FooClassBehavior"); + + //Clear out what was done during this test + $anothercomponent->unlisten(); + $this->component->detachClassBehavior('FooClassBehavior'); + $this->component->detachClassBehavior('BarClassBehavior'); + + // Test attaching of single object behaviors as class-wide behaviors + $this->component->attachClassBehavior('BarBehaviorObject', 'BarBehavior'); + $this->assertTrue($this->component->asa('BarBehaviorObject') instanceof BarBehavior); + $this->assertEquals($this->component->BarBehaviorObject->Owner, $this->component); + $this->component->detachClassBehavior('BarBehaviorObject'); + } + + + + + + public function testDetachClassBehavior() { + // ensure that the component is listening + $this->assertEquals(1, $this->component->getEventHandlers('fxDetachClassBehavior')->getCount()); + + $prenolistencomponent = new NewComponentNoListen(); + + //Attach a class behavior + $this->component->attachClassBehavior('FooClassBehavior', new FooClassBehavior); + + //Create new components that listen and don't listen to global events + $anothercomponent = new NewComponent(); + $postnolistencomponent = new NewComponentNoListen(); + + //ensures that all the Components are properly initialized + $this->assertEquals(2, $this->component->getEventHandlers('fxDetachClassBehavior')->getCount()); + $this->assertNotNull($this->component->asa('FooClassBehavior'), "Component does not have the FooClassBehavior and should have this behavior"); + $this->assertNull($prenolistencomponent->asa('FooClassBehavior'), "Component has the FooClassBehavior and should _not_ have this behavior"); + $this->assertNotNull($anothercomponent->asa('FooClassBehavior'), "Component does not have the FooClassBehavior and should have this behavior"); + $this->assertNotNull($postnolistencomponent->asa('FooClassBehavior'), "Component does not have the FooClassBehavior and should have this behavior"); + + + $this->component->detachClassBehavior('FooClassBehavior'); + + $this->assertNull($this->component->asa('FooClassBehavior'), "Component has the FooClassBehavior and should _not_ have this behavior"); + $this->assertNull($prenolistencomponent->asa('FooClassBehavior'), "Component has the FooClassBehavior and should _not_ have this behavior"); + $this->assertNull($anothercomponent->asa('FooClassBehavior'), "Component has the FooClassBehavior and should _not_ have this behavior"); + $this->assertNotNull($postnolistencomponent->asa('FooClassBehavior'), "Component does not have the FooClassBehavior and should have this behavior"); + + + //tear down function variables + $anothercomponent->unlisten(); + } + + public function testGetClassHierarchy() { + $component = new DynamicCatchingComponent; + $this->assertEquals(array('TComponent', 'NewComponent', 'NewComponentNoListen', 'DynamicCatchingComponent'), $component->getClassHierarchy()); + $this->assertEquals(array('TComponent', 'NewComponent', 'NewComponentNoListen', 'DynamicCatchingComponent'), $component->getClassHierarchy(false)); + $this->assertEquals(array('tcomponent', 'newcomponent', 'newcomponentnolisten', 'dynamiccatchingcomponent'), $component->getClassHierarchy(true)); + } + + + public function testAsA() { + $anothercomponent = new NewComponent(); + + // ensure the component does not have the FooClassBehavior + $this->assertNull($this->component->asa('FooClassBehavior')); + $this->assertNull($this->component->asa('FooFooClassBehavior')); + $this->assertNull($this->component->asa('BarClassBehavior')); + $this->assertNull($this->component->asa('NonExistantClassBehavior')); + + $this->assertNull($anothercomponent->asa('FooClassBehavior')); + $this->assertNull($anothercomponent->asa('FooFooClassBehavior')); + $this->assertNull($anothercomponent->asa('BarClassBehavior')); + $this->assertNull($anothercomponent->asa('NonExistantClassBehavior')); + + // add the class behavior + $this->component->attachClassBehavior('FooClassBehavior', new FooClassBehavior); + + //Check that the component has only the class behavior assigned + $this->assertNotNull($this->component->asa('FooClassBehavior')); + $this->assertNull($this->component->asa('FooFooClassBehavior')); + $this->assertNull($this->component->asa('BarClassBehavior')); + $this->assertNull($this->component->asa('NonExistantClassBehavior')); + + //Check that the component has only the class behavior assigned + $this->assertNotNull($anothercomponent->asa('FooClassBehavior')); + $this->assertNull($anothercomponent->asa('FooFooClassBehavior')); + $this->assertNull($anothercomponent->asa('BarClassBehavior')); + $this->assertNull($anothercomponent->asa('NonExistantClassBehavior')); + + // remove the class behavior + $this->component->detachClassBehavior('FooClassBehavior'); + + // Check the function doesn't have the behavior any more + $this->assertNull($this->component->asa('FooClassBehavior')); + $this->assertNull($this->component->asa('FooFooClassBehavior')); + $this->assertNull($this->component->asa('BarClassBehavior')); + $this->assertNull($this->component->asa('NonExistantClassBehavior')); + + $this->assertNull($anothercomponent->asa('FooClassBehavior')); + $this->assertNull($anothercomponent->asa('FooFooClassBehavior')); + $this->assertNull($anothercomponent->asa('BarClassBehavior')); + $this->assertNull($anothercomponent->asa('NonExistantClassBehavior')); + + + + + $this->component->attachBehavior('BarBehavior', new BarBehavior); + + //Check that the component has only the object behavior assigned + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('FooFooBehavior')); + $this->assertNotNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('NonExistantBehavior')); + + //Check that the component has the behavior assigned + $this->assertNull($anothercomponent->asa('FooBehavior')); + $this->assertNull($anothercomponent->asa('FooFooBehavior')); + $this->assertNull($anothercomponent->asa('BarBehavior')); + $this->assertNull($anothercomponent->asa('NonExistantBehavior')); + + $this->component->detachBehavior('BarBehavior'); + + //Check that the component has no object behaviors assigned + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('FooFooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('NonExistantBehavior')); + + //Check that the component has no behavior assigned + $this->assertNull($anothercomponent->asa('FooBehavior')); + $this->assertNull($anothercomponent->asa('FooFooBehavior')); + $this->assertNull($anothercomponent->asa('BarBehavior')); + $this->assertNull($anothercomponent->asa('NonExistantBehavior')); + + $anothercomponent->unlisten(); + } + + public function testIsA() { + //This doesn't check the IInstanceCheck functionality, separate function + + $this->assertTrue($this->component->isa('TComponent')); + $this->assertTrue($this->component->isa('NewComponent')); + $this->assertFalse($this->component->isa(new FooBehavior)); + $this->assertFalse($this->component->isa('FooBehavior')); + + //Ensure there is no BarBehavior + $this->assertNull($this->component->asa('FooFooBehavior')); + + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('FooFooBehavior')); + + $this->component->attachBehavior('FooFooBehavior', new FooFooBehavior); + + $this->assertNotNull($this->component->asa('FooFooBehavior')); + + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertTrue($this->component->isa('FooFooBehavior')); + + $this->component->disableBehaviors(); + // It still has the behavior + $this->assertNotNull($this->component->asa('FooFooBehavior')); + + // But it is not expressed + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('FooFooBehavior')); + + $this->component->enableBehaviors(); + $this->assertNotNull($this->component->asa('FooFooBehavior')); + + $this->assertTrue($this->component->isa('FooFooBehavior')); + + + + $this->component->attachBehavior('FooBarBehavior', new FooBarBehavior); + + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertTrue($this->component->isa('FooBarBehavior')); + + $this->component->disableBehavior('FooBarBehavior'); + + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('FooBarBehavior')); + + $this->component->enableBehavior('FooBarBehavior'); + $this->component->disableBehavior('FooFooBehavior'); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('FooFooBehavior')); + $this->assertTrue($this->component->isa('FooBarBehavior')); + + $this->component->disableBehavior('FooBarBehavior'); + $this->component->disableBehavior('FooFooBehavior'); + + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('FooFooBehavior')); + $this->assertFalse($this->component->isa('FooBarBehavior')); + + $this->component->enableBehavior('FooBarBehavior'); + $this->component->enableBehavior('FooFooBehavior'); + + $this->assertTrue($this->component->isa('FooFooBehavior')); + $this->assertTrue($this->component->isa('FooBarBehavior')); + + + $this->component->detachBehavior('FooFooBehavior'); + $this->component->detachBehavior('FooBarBehavior'); + + $this->assertFalse($this->component->isa(new FooBehavior)); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa(new FooFooBehavior)); + $this->assertFalse($this->component->isa('FooFooBehavior')); + $this->assertFalse($this->component->isa(new FooBarBehavior)); + $this->assertFalse($this->component->isa('FooBarBehavior')); + + } + + public function testIsA_with_IInstanceCheck() { + + $this->assertTrue($this->component->isa('NewComponent')); + $this->assertFalse($this->component->isa('PreBarBehavior')); + + $this->component->attachBehavior('BarBehavior', $behavior = new BarBehavior); + + $behavior->setInstanceReturn(null); + + $this->assertTrue($this->component->isa('NewComponent')); + $this->assertTrue($this->component->isa('PreBarBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + + // This forces the iso on the BarBehavior to respond to any class with false + $behavior->setInstanceReturn(false); + $this->assertFalse($this->component->isa('PreBarBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + + //This forces the isa on the BarBehavior to respond to any class with true + $behavior->setInstanceReturn(true); + $this->assertTrue($this->component->isa('FooBehavior')); + + + } + + public function testAttachDetachBehavior() { + + try { + $this->component->faaEverMore(true, true); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TApplicationException $e) {} + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertFalse($this->component->isa('BarBehavior')); + + try { + $this->component->attachBehavior('FooBehavior', new TComponent); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TInvalidDataTypeException $e) {} + + $this->component->attachBehavior('FooBehavior', new FooBehavior); + + $this->assertNotNull($this->component->asa('FooBehavior')); + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertFalse($this->component->isa('BarBehavior')); + + try { + $this->assertTrue($this->component->faaEverMore(true, true)); + } catch(TApplicationException $e) { + $this->fail('TApplicationException raised while trying to execute a behavior class method'); + } + + try { + $this->component->noMethodHere(true); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TApplicationException $e) {} + + $this->assertTrue($this->component->disableBehavior('FooBehavior')); + + //BarBehavior is not a behavior at this time + $this->assertNull($this->component->disableBehavior('BarBehavior')); + + try { + $this->component->faaEverMore(true, true); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TApplicationException $e) {} + + $this->assertTrue($this->component->enableBehavior('FooBehavior')); + + //BarBehavior is not a behavior at this time + $this->assertNull($this->component->enableBehavior('BarBehavior')); + + try { + $this->assertTrue($this->component->faaEverMore(true, true)); + } catch(TApplicationException $e) { + $this->fail('TApplicationException raised while trying to execute a behavior class method'); + } + + $this->component->detachBehavior('FooBehavior'); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertFalse($this->component->isa('BarBehavior')); + + } + + public function testAttachDetachBehaviors() { + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNull($this->component->asa('PreBarBehavior')); + + $this->component->attachBehaviors(array('FooFooBehavior' => new FooFooBehavior, 'BarBehavior' => new BarBehavior, 'PreBarBehavior' => new PreBarBehavior)); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNotNull($this->component->asa('FooFooBehavior')); + $this->assertNotNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNotNull($this->component->asa('PreBarBehavior')); + + $this->assertTrue($this->component->isa('FooFooBehavior')); + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertTrue($this->component->isa('BarBehavior')); + $this->assertTrue($this->component->isa('PreBarBehavior')); + $this->assertFalse($this->component->isa('FooBarBehavior')); + + $this->component->detachBehaviors(array('FooFooBehavior' => new FooFooBehavior, 'BarBehavior' => new BarBehavior)); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('FooFooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNotNull($this->component->asa('PreBarBehavior')); + + $this->assertFalse($this->component->isa('FooFooBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('BarBehavior')); + $this->assertFalse($this->component->isa('FooBarBehavior')); + $this->assertTrue($this->component->isa('PreBarBehavior')); + + + + // testing if we can detachBehaviors just by the name of the behavior instead of an array of the behavior + $this->component->attachBehaviors(array('FooFooBehavior' => new FooFooBehavior, 'BarBehavior' => new BarBehavior)); + + $this->assertTrue($this->component->isa('FooBehavior')); + $this->assertTrue($this->component->isa('BarBehavior')); + + $this->component->detachBehaviors(array('FooFooBehavior', 'BarBehavior')); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('FooFooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + + $this->assertFalse($this->component->isa('FooFooBehavior')); + $this->assertFalse($this->component->isa('FooBehavior')); + $this->assertFalse($this->component->isa('BarBehavior')); + $this->assertFalse($this->component->isa('FooBarBehavior')); + } + + + public function testClearBehaviors() { + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNull($this->component->asa('PreBarBehavior')); + + $this->component->attachBehaviors(array('FooFooBehavior' => new FooFooBehavior, 'BarBehavior' => new BarBehavior, 'PreBarBehavior' => new PreBarBehavior)); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNotNull($this->component->asa('FooFooBehavior')); + $this->assertNotNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNotNull($this->component->asa('PreBarBehavior')); + + $this->component->clearBehaviors(); + + $this->assertNull($this->component->asa('FooBehavior')); + $this->assertNull($this->component->asa('BarBehavior')); + $this->assertNull($this->component->asa('FooBarBehavior')); + $this->assertNull($this->component->asa('PreBarBehavior')); + } + + public function testEnableDisableBehavior() { + + $this->assertNull($this->component->enableBehavior('FooBehavior')); + $this->assertNull($this->component->disableBehavior('FooBehavior')); + + try { + $this->component->faaEverMore(true, true); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TApplicationException $e) {} + + $this->component->attachBehavior('FooBehavior', new FooBehavior); + + $this->assertTrue($this->component->isa('FooBehavior')); + try { + $this->assertTrue($this->component->faaEverMore(true, true)); + } catch(TApplicationException $e) { + $this->fail('TApplicationException raised while trying to execute a behavior class method'); + } + + $this->assertTrue($this->component->disableBehavior('FooBehavior')); + + $this->assertFalse($this->component->isa('FooBehavior')); + + try { + $this->component->faaEverMore(true, true); + $this->fail('TApplicationException not raised trying to execute a undefined class method'); + } catch(TApplicationException $e) {} + + $this->assertTrue($this->component->enableBehavior('FooBehavior')); + + $this->assertTrue($this->component->isa('FooBehavior')); + try { + $this->assertTrue($this->component->faaEverMore(true, true)); + } catch(TApplicationException $e) { + $this->fail('TApplicationException raised while trying to execute a behavior class method'); + } + + + + $this->assertNull($this->component->enableBehavior('BarClassBehavior')); + $this->assertNull($this->component->disableBehavior('BarClassBehavior')); + + try { + $this->component->moreFunction(true, true); + $this->fail('TApplicationException not raised trying to execute an undefined class method'); + } catch(TApplicationException $e) {} + + $this->component->attachClassBehavior('BarClassBehavior', new BarClassBehavior); + + $this->assertFalse($this->component->enableBehavior('BarClassBehavior')); + $this->assertFalse($this->component->disableBehavior('BarClassBehavior')); + + try { + $this->assertTrue($this->component->moreFunction(true, true)); + } catch(TApplicationException $e) { + $this->fail('TApplicationException raised while trying to execute a behavior class method'); + } + + $this->component->detachClassBehavior('BarClassBehavior'); + } + + + public function testBehaviorFunctionCalls() { + + $this->component->attachBehavior('FooBarBehavior', $behavior = new FooBarBehavior); + $this->component->attachClassBehavior('FooClassBehavior', $classbehavior = new FooClassBehavior); + + // Test the Class Methods + $this->assertEquals(12, $this->component->faaEverMore(3, 4)); + + // Check that the called object is shifted in front of the array of a class behavior call + $this->assertEquals($this->component, $this->component->getLastClassObject()); + + + //Test the FooBarBehavior + $this->assertEquals(27, $this->component->moreFunction(3, 3)); + + $this->assertTrue($this->component->disableBehavior('FooBarBehavior')); + try { + $this->assertNull($this->component->moreFunction(3, 4)); + $this->fail('TApplicationException not raised trying to execute a disabled behavior'); + } catch(TApplicationException $e) {} + $this->assertTrue($this->component->enableBehavior('FooBarBehavior')); + + // Test the global event space, this should work and return false because no function implements these methods + $this->assertNull($this->component->fxSomeUndefinedGlobalEvent()); + $this->assertNull($this->component->dySomeUndefinedIntraObjectEvent()); + + $this->component->detachClassBehavior('FooClassBehavior'); + + + + // test object instance behaviors implemented through class-wide behaviors + $this->component->attachClassBehavior('FooFooBehaviorAsClass', 'FooFooBehavior'); + + $component = new NewComponent; + + $this->assertEquals(5, $this->component->faafaaEverMore(3, 4)); + $this->assertEquals(10, $component->faafaaEverMore(6, 8)); + + $this->component->detachClassBehavior('FooFooBehaviorAsClass'); + $component->unlisten(); + $component = null; + + try { + $this->component->faafaaEverMore(3, 4); + $this->fail('TApplicationException not raised trying to execute a disabled behavior'); + } catch(TApplicationException $e) {} + + + + // make a call to an unpatched fx and dy call so that it's passed through to the __dycall function + $dynamicComponent = new DynamicCallComponent; + + $this->assertNull($dynamicComponent->fxUndefinedEvent()); + $this->assertNull($dynamicComponent->dyUndefinedEvent()); + + //This tests the dynamic __dycall function + $this->assertEquals(1024, $dynamicComponent->dyPowerFunction(2, 10)); + $this->assertEquals(5, $dynamicComponent->dyDivisionFunction(10, 2)); + + $this->assertEquals(2048, $dynamicComponent->fxPowerFunction(2, 10)); + $this->assertEquals(10, $dynamicComponent->fxDivisionFunction(10, 2)); + + $dynamicComponent->unlisten(); + + } + public function testHasProperty() { $this->assertTrue($this->component->hasProperty('Text'), "Component hasn't property Text"); $this->assertTrue($this->component->hasProperty('text'), "Component hasn't property text"); - $this->assertFalse($this->component->hasProperty('Caption'), "Component as property Caption"); + $this->assertFalse($this->component->hasProperty('Caption'), "Component has property Caption"); + + $this->assertTrue($this->component->hasProperty('ColorAttribute'), "Component hasn't property JsColorAttribute"); + $this->assertTrue($this->component->hasProperty('colorattribute'), "Component hasn't property JsColorAttribute"); + $this->assertFalse($this->component->canGetProperty('PastelAttribute'), "Component has property JsPastelAttribute"); + + $this->assertTrue($this->component->hasProperty('JSColorAttribute'), "Component hasn't property JsColorAttribute"); + $this->assertTrue($this->component->hasProperty('jscolorattribute'), "Component hasn't property JsColorAttribute"); + $this->assertFalse($this->component->hasProperty('jsPastelAttribute'), "Component has property JsPastelAttribute"); + + $this->assertFalse($this->component->hasProperty('Excitement'), "Component has property Excitement"); + $this->component->attachBehavior('ExcitementPropBehavior', new BehaviorTestBehavior); + $this->assertTrue($this->component->hasProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehaviors(); + $this->assertFalse($this->component->hasProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehaviors(); + $this->assertTrue($this->component->hasProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehavior('ExcitementPropBehavior'); + $this->assertFalse($this->component->hasProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehavior('ExcitementPropBehavior'); + $this->assertTrue($this->component->hasProperty('Excitement'), "Component hasn't property Excitement"); + + $this->component->detachBehavior('ExcitementPropBehavior'); + + $this->assertFalse($this->component->hasProperty('Excitement'), "Component has property Excitement"); + } public function testCanGetProperty() { $this->assertTrue($this->component->canGetProperty('Text')); $this->assertTrue($this->component->canGetProperty('text')); $this->assertFalse($this->component->canGetProperty('Caption')); + + $this->assertTrue($this->component->canGetProperty('ColorAttribute')); + $this->assertTrue($this->component->canGetProperty('colorattribute')); + $this->assertFalse($this->component->canGetProperty('PastelAttribute')); + + $this->assertTrue($this->component->canGetProperty('JSColorAttribute')); + $this->assertTrue($this->component->canGetProperty('jscolorattribute')); + $this->assertFalse($this->component->canGetProperty('jsPastelAttribute')); + + + $this->assertFalse($this->component->canGetProperty('Excitement'), "Component has property Excitement"); + $this->component->attachBehavior('ExcitementPropBehavior', new BehaviorTestBehavior); + $this->assertTrue($this->component->canGetProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehaviors(); + $this->assertFalse($this->component->canGetProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehaviors(); + $this->assertTrue($this->component->canGetProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehavior('ExcitementPropBehavior'); + $this->assertFalse($this->component->canGetProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehavior('ExcitementPropBehavior'); + $this->assertTrue($this->component->canGetProperty('Excitement'), "Component hasn't property Excitement"); + + $this->component->detachBehavior('ExcitementPropBehavior'); + + $this->assertFalse($this->component->canGetProperty('Excitement'), "Component has property Excitement"); } public function testCanSetProperty() { $this->assertTrue($this->component->canSetProperty('Text')); $this->assertTrue($this->component->canSetProperty('text')); $this->assertFalse($this->component->canSetProperty('Caption')); + + $this->assertTrue($this->component->canSetProperty('ColorAttribute')); + $this->assertTrue($this->component->canSetProperty('colorattribute')); + $this->assertFalse($this->component->canSetProperty('PastelAttribute')); + + $this->assertTrue($this->component->canSetProperty('JSColorAttribute')); + $this->assertTrue($this->component->canSetProperty('jscolorattribute')); + $this->assertFalse($this->component->canSetProperty('jsPastelAttribute')); + + $this->assertFalse($this->component->canSetProperty('Excitement'), "Component has property Excitement"); + $this->component->attachBehavior('ExcitementPropBehavior', new BehaviorTestBehavior); + $this->assertTrue($this->component->canSetProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehaviors(); + $this->assertFalse($this->component->canSetProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehaviors(); + $this->assertTrue($this->component->canSetProperty('Excitement'), "Component hasn't property Excitement"); + $this->component->disableBehavior('ExcitementPropBehavior'); + $this->assertFalse($this->component->canSetProperty('Excitement'), "Component has property Excitement"); + $this->component->enableBehavior('ExcitementPropBehavior'); + $this->assertTrue($this->component->canSetProperty('Excitement'), "Component hasn't property Excitement"); + + $this->component->detachBehavior('ExcitementPropBehavior'); } public function testGetProperty() { @@ -75,6 +1181,67 @@ class TComponentTest extends PHPUnit_Framework_TestCase { $this->fail('exception not raised when getting undefined property'); } catch(TInvalidOperationException $e) { } + + $this->assertTrue($this->component->OnMyEvent instanceof TPriorityList); + try { + $value2=$this->component->onUndefinedEvent; + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + //Without the function parenthesis, the function is _not_ called but the __get + // method is called and the global events (list) are accessed + $this->assertTrue($this->component->fxAttachClassBehavior instanceof TPriorityList); + $this->assertTrue($this->component->fxDetachClassBehavior instanceof TPriorityList); + + // even undefined global events have a list as every object is able to access every event + $this->assertTrue($this->component->fxUndefinedEvent instanceof TPriorityList); + + + // Test the behaviors within the __get function + $this->component->enableBehaviors(); + + try { + $value2=$this->component->Excitement; + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + $this->component->attachBehavior('BehaviorTestBehavior', $behavior = new BehaviorTestBehavior); + $this->assertEquals('faa', $this->component->Excitement); + + $this->component->disableBehaviors(); + + try { + $this->assertEquals('faa', $this->component->Excitement); + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + $this->component->enableBehaviors(); + $this->assertEquals('faa', $this->component->getExcitement()); + + $this->component->disableBehavior('BehaviorTestBehavior'); + + $this->assertEquals($behavior, $this->component->BehaviorTestBehavior); + try { + $behavior = $this->component->BehaviorTestBehavior2; + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + try { + $this->assertEquals('faa', $this->component->Excitement); + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->assertEquals('faa', $this->component->getExcitement()); + + + // behaviors allow on and fx events to be passed through. + $this->assertTrue($this->component->onBehaviorEvent instanceof TPriorityList); + } public function testSetProperty() { @@ -87,6 +1254,203 @@ class TComponentTest extends PHPUnit_Framework_TestCase { $this->fail('exception not raised when setting undefined property'); } catch(TInvalidOperationException $e) { } + + // Test get only properties is a set function + try { + $this->component->ReadOnlyProperty = 'setting read only'; + $this->fail('a property without a set function was set to a new value without error'); + } catch(TInvalidOperationException $e) { + } + + try { + $this->component->ReadOnlyJsProperty = 'jssetting read only'; + $this->fail('a js property without a set function was set to a new value without error'); + } catch(TInvalidOperationException $e) { + } + + try { + $this->component->JsReadOnlyJsProperty = 'jssetting read only'; + $this->fail('a js property without a set function was set to a new value without error'); + } catch(TInvalidOperationException $e) { + } + + $this->assertEquals(0, $this->component->getEventHandlers('onMyEvent')->getCount()); + $this->component->onMyEvent = array($this->component,'myEventHandler'); + $this->assertEquals(1, $this->component->getEventHandlers('onMyEvent')->getCount()); + $this->component->onMyEvent[] = array($this->component,'Object.myEventHandler'); + $this->assertEquals(2, $this->component->getEventHandlers('onMyEvent')->getCount()); + + $this->component->getEventHandlers('onMyEvent')->clear(); + + // Test the behaviors within the __get function + $this->component->enableBehaviors(); + + try { + $this->component->Excitement = 'laa'; + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + $this->component->attachBehavior('BehaviorTestBehavior', $behavior1 = new BehaviorTestBehavior); + $this->component->Excitement = 'laa'; + $this->assertEquals('laa', $this->component->Excitement); + $this->assertEquals('sol', $this->component->Excitement = 'sol'); + + + $this->component->disableBehaviors(); + + try { + $this->component->Excitement = false; + $this->assertEquals(false, $this->component->Excitement); + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + + $this->component->enableBehaviors(); + $this->component->Excitement = 'faa'; + $this->assertEquals('faa', $this->component->getExcitement()); + + $this->component->disableBehavior('BehaviorTestBehavior'); + + try { + $this->component->Excitement = false; + $this->assertEquals(false, $this->component->Excitement); + $this->fail('exception not raised when getting undefined property'); + } catch(TInvalidOperationException $e) { + } + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->component->Excitement = 'sol'; + $this->assertEquals('sol', $this->component->Excitement); + + + $this->component->attachBehavior('BehaviorTestBehavior2', $behavior2 = new BehaviorTestBehavior); + + $this->assertEquals('sol', $this->component->Excitement); + $this->assertEquals('faa', $behavior2->Excitement); + + // this sets Excitement for both because they are not uniquely named + $this->component->Excitement = 'befaad'; + + $this->assertEquals('befaad', $this->component->Excitement); + $this->assertEquals('befaad', $behavior1->Excitement); + $this->assertEquals('befaad', $behavior2->Excitement); + + + $this->component->detachBehavior('BehaviorTestBehavior2'); + + // behaviors allow on and fx events to be passed through. + $this->assertTrue($this->component->BehaviorTestBehavior->onBehaviorEvent instanceof TPriorityList); + + $this->assertEquals(0, $this->component->BehaviorTestBehavior->getEventHandlers('onBehaviorEvent')->getCount()); + $this->component->onBehaviorEvent = array($this->component,'myEventHandler'); + $this->assertEquals(1, $this->component->BehaviorTestBehavior->getEventHandlers('onBehaviorEvent')->getCount()); + $this->component->onBehaviorEvent[] = array($this->component,'Object.myEventHandler'); + $this->assertEquals(2, $this->component->BehaviorTestBehavior->getEventHandlers('onBehaviorEvent')->getCount()); + + $this->component->BehaviorTestBehavior->getEventHandlers('onBehaviorEvent')->clear(); + } + + + public function testIsSetFunction() { + $this->assertTrue(isset($this->component->fxAttachClassBehavior)); + $this->component->unlisten(); + + $this->assertFalse(isset($this->component->onMyEvent)); + $this->assertFalse(isset($this->component->undefinedEvent)); + $this->assertFalse(isset($this->component->fxAttachClassBehavior)); + + $this->assertFalse(isset($this->component->BehaviorTestBehavior)); + $this->assertFalse(isset($this->component->onBehaviorEvent)); + + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + + $this->assertTrue(isset($this->component->BehaviorTestBehavior)); + $this->assertFalse(isset($this->component->onBehaviorEvent)); + + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->assertTrue(isset($this->component->onBehaviorEvent)); + + $this->component->attachEventHandler('onMyEvent','foo'); + $this->assertTrue(isset($this->component->onMyEvent)); + + $this->assertTrue(isset($this->component->Excitement)); + $this->component->Excitement = null; + $this->assertFalse(isset($this->component->Excitement)); + $this->assertFalse(isset($this->component->UndefinedBehaviorProperty)); + + + } + + + public function testUnsetFunction() { + + $this->assertEquals('default', $this->component->getText()); + unset($this->component->Text); + $this->assertNull($this->component->getText()); + + unset($this->component->UndefinedProperty); + + // object events + $this->assertEquals(0, $this->component->onMyEvent->Count); + $this->component->attachEventHandler('onMyEvent','foo'); + $this->assertEquals(1, $this->component->onMyEvent->Count); + unset($this->component->onMyEvent); + $this->assertEquals(0, $this->component->onMyEvent->Count); + + //global events + $this->assertEquals(1, $this->component->fxAttachClassBehavior->Count); + $component = new NewComponent(); + $this->assertEquals(2, $this->component->fxAttachClassBehavior->Count); + unset($this->component->fxAttachClassBehavior); + // retain the other object event + $this->assertEquals(1, $this->component->fxAttachClassBehavior->Count); + $component->unlisten(); + + try { + unset($this->component->Object); + $this->fail('TInvalidOperationException not raised when unsetting get only property'); + } catch(TInvalidOperationException $e) {} + + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + $this->assertTrue($this->component->asa('BehaviorTestBehavior') instanceof BehaviorTestBehavior); + $this->assertFalse($this->component->asa('BehaviorTestBehavior2') instanceof BehaviorTestBehavior); + + $this->assertEquals('faa', $this->component->Excitement); + unset($this->component->Excitement); + $this->assertNull($this->component->Excitement); + $this->component->Excitement = 'sol'; + $this->assertEquals('sol', $this->component->Excitement); + + // Test the disabling of unset within behaviors + $this->component->disableBehaviors(); + unset($this->component->Excitement); + $this->component->enableBehaviors(); + // This should still be 'sol' because the unset happened inside behaviors being disabled + $this->assertEquals('sol', $this->component->Excitement); + $this->component->disableBehavior('BehaviorTestBehavior'); + unset($this->component->Excitement); + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->assertEquals('sol', $this->component->Excitement); + + unset($this->component->Excitement); + $this->assertNull($this->component->Excitement); + + try { + unset($this->component->ReadOnly); + $this->fail('TInvalidOperationException not raised when unsetting get only property'); + } catch(TInvalidOperationException $e) {} + + $this->component->onBehaviorEvent = 'foo'; + $this->assertEquals(1, count($this->component->onBehaviorEvent)); + $this->assertEquals(1, count($this->component->BehaviorTestBehavior->onBehaviorEvent)); + unset($this->component->onBehaviorEvent); + $this->assertEquals(0, count($this->component->onBehaviorEvent)); + $this->assertEquals(0, count($this->component->BehaviorTestBehavior->onBehaviorEvent)); + + // Remove behavior via unset + unset($this->component->BehaviorTestBehavior); + $this->assertFalse($this->component->asa('BehaviorTestBehavior') instanceof BehaviorTestBehavior); + } public function testGetSubProperty() { @@ -102,46 +1466,225 @@ class TComponentTest extends PHPUnit_Framework_TestCase { $this->assertTrue($this->component->hasEvent('OnMyEvent')); $this->assertTrue($this->component->hasEvent('onmyevent')); $this->assertFalse($this->component->hasEvent('onYourEvent')); + + // fx won't throw an error if any of these fx function are called on an object. + // It is a special prefix event designation that every object responds to all events. + $this->assertTrue($this->component->hasEvent('fxAttachClassBehavior')); + $this->assertTrue($this->component->hasEvent('fxattachclassbehavior')); + + $this->assertTrue($this->component->hasEvent('fxNonExistantGlobalEvent')); + $this->assertTrue($this->component->hasEvent('fxnonexistantglobalevent')); + + $this->assertTrue($this->component->hasEvent('dyNonExistantLocalEvent')); + $this->assertTrue($this->component->hasEvent('fxnonexistantlocalevent')); + + + //Test behavior events + $this->assertFalse($this->component->hasEvent('onBehaviorEvent')); + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + $this->assertTrue($this->component->hasEvent('onBehaviorEvent')); + $this->assertTrue($this->component->BehaviorTestBehavior->hasEvent('onBehaviorEvent')); + + $this->component->disableBehavior('BehaviorTestBehavior'); + $this->assertFalse($this->component->hasEvent('onBehaviorEvent')); + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->assertTrue($this->component->hasEvent('onBehaviorEvent')); } public function testHasEventHandler() { $this->assertFalse($this->component->hasEventHandler('OnMyEvent')); $this->component->attachEventHandler('OnMyEvent','foo'); $this->assertTrue($this->component->hasEventHandler('OnMyEvent')); + + $this->assertFalse($this->component->hasEventHandler('fxNonExistantGlobalEvent')); + $this->component->attachEventHandler('fxNonExistantGlobalEvent','foo'); + $this->assertTrue($this->component->hasEventHandler('fxNonExistantGlobalEvent')); + + //Test behavior events + $this->assertFalse($this->component->hasEventHandler('onBehaviorEvent')); + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + $this->assertFalse($this->component->hasEventHandler('onBehaviorEvent')); + $this->assertFalse($this->component->BehaviorTestBehavior->hasEventHandler('onBehaviorEvent')); + + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->assertTrue($this->component->hasEventHandler('onBehaviorEvent')); + + $this->component->disableBehavior('BehaviorTestBehavior'); + $this->assertFalse($this->component->hasEvent('onBehaviorEvent')); + $this->assertFalse($this->component->hasEventHandler('onBehaviorEvent')); + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->assertTrue($this->component->hasEvent('onBehaviorEvent')); + $this->assertTrue($this->component->hasEventHandler('onBehaviorEvent')); } public function testGetEventHandlers() { $list=$this->component->getEventHandlers('OnMyEvent'); - $this->assertTrue(($list instanceof TList) && ($list->getCount()===0)); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===0)); $this->component->attachEventHandler('OnMyEvent','foo'); - $this->assertTrue(($list instanceof TList) && ($list->getCount()===1)); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===1)); try { $list=$this->component->getEventHandlers('YourEvent'); $this->fail('exception not raised when getting event handlers for undefined event'); } catch(TInvalidOperationException $e) { } + + $list=$this->component->getEventHandlers('fxRandomEvent'); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===0)); + $this->component->attachEventHandler('fxRandomEvent','foo'); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===1)); + try { + $list=$this->component->getEventHandlers('fxSomeUndefinedGlobalEvent'); + } catch(TInvalidOperationException $e) { + $this->fail('exception raised when getting event handlers for universal global event'); + } + + + + //Test behavior events + try { + $list=$this->component->getEventHandlers('onBehaviorEvent'); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) { + } + $this->assertFalse($this->component->hasEventHandler('onBehaviorEvent')); + + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + $list=$this->component->getEventHandlers('onBehaviorEvent'); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===0)); + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->assertTrue(($list instanceof TPriorityList) && ($list->getCount()===1)); + + $this->component->disableBehavior('BehaviorTestBehavior'); + try { + $list=$this->component->getEventHandlers('onBehaviorEvent'); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) { + } + $this->component->enableBehavior('BehaviorTestBehavior'); + $this->assertTrue(($this->component->getEventHandlers('onBehaviorEvent') instanceof TPriorityList) && ($list->getCount()===1)); + } public function testAttachEventHandler() { + $this->component->attachEventHandler('OnMyEvent','foo'); - $this->assertTrue($this->component->getEventHandlers('OnMyEvent')->getCount()===1); + $this->assertEquals(1, $this->component->getEventHandlers('OnMyEvent')->getCount()); try { $this->component->attachEventHandler('YourEvent','foo'); $this->fail('exception not raised when attaching event handlers for undefined event'); } catch(TInvalidOperationException $e) { } - /*$this->component->MyEvent[]='foo2'; - $this->assertTrue($this->component->getEventHandlers('MyEvent')->getCount()===2); - $this->component->getEventHandlers('MyEvent')->add('foo3'); - $this->assertTrue($this->component->getEventHandlers('MyEvent')->getCount()===3); - $this->component->MyEvent[0]='foo4'; - $this->assertTrue($this->component->getEventHandlers('MyEvent')->getCount()===3); - $this->component->getEventHandlers('MyEvent')->insert(0,'foo5'); - $this->assertTrue($this->component->MyEvent->Count===4 && $this->component->MyEvent[0]==='foo5'); - $this->component->MyEvent='foo6'; - $this->assertTrue($this->component->MyEvent->Count===5 && $this->component->MyEvent[4]==='foo6');*/ + + //Testing the priorities of attaching events + $this->component->attachEventHandler('OnMyEvent','foopre', 5); + $this->component->attachEventHandler('OnMyEvent','foopost', 15); + $this->component->attachEventHandler('OnMyEvent','foobar', 10); + $this->assertEquals(4, $this->component->getEventHandlers('OnMyEvent')->getCount()); + $list = $this->component->getEventHandlers('OnMyEvent'); + $this->assertEquals('foopre', $list[0]); + $this->assertEquals('foo', $list[1]); + $this->assertEquals('foobar', $list[2]); + $this->assertEquals('foopost', $list[3]); + + + //Test attaching behavior events + try { + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) { + } + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + + $this->component->attachEventHandler('onBehaviorEvent','foo'); + + //Testing the priorities of attaching behavior events + $this->component->attachEventHandler('onBehaviorEvent','foopre', 5); + $this->component->attachEventHandler('onBehaviorEvent','foopost', 15); + $this->component->attachEventHandler('onBehaviorEvent','foobar', 10); + $this->component->attachEventHandler('onBehaviorEvent','foobarfoobar', 10); + $this->assertEquals(5, $this->component->getEventHandlers('onBehaviorEvent')->getCount()); + $list = $this->component->getEventHandlers('onBehaviorEvent'); + $this->assertEquals('foopre', $list[0]); + $this->assertEquals('foo', $list[1]); + $this->assertEquals('foobar', $list[2]); + $this->assertEquals('foobarfoobar', $list[3]); + $this->assertEquals('foopost', $list[4]); + + $this->component->disableBehavior('BehaviorTestBehavior'); + try { + $this->component->attachEventHandler('onBehaviorEvent','bar'); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) { + } + $this->component->enableBehavior('BehaviorTestBehavior'); + } + public function testDetachEventHandler() { + + $this->component->attachEventHandler('OnMyEvent','foo'); + $this->assertEquals(1, $this->component->getEventHandlers('OnMyEvent')->getCount()); + + $this->component->attachEventHandler('OnMyEvent','foopre', 5); + $this->component->attachEventHandler('OnMyEvent','foopost', 15); + $this->component->attachEventHandler('OnMyEvent','foobar', 10); + $this->component->attachEventHandler('OnMyEvent','foobarfoobar', 10); + + + + $this->component->detachEventHandler('OnMyEvent','foo'); + $list = $this->component->getEventHandlers('OnMyEvent'); + $this->assertEquals(4, $list->getCount()); + + $this->assertEquals('foopre', $list[0]); + $this->assertEquals('foobar', $list[1]); + $this->assertEquals('foobarfoobar', $list[2]); + $this->assertEquals('foopost', $list[3]); + + $this->component->detachEventHandler('OnMyEvent','foopre', null); + $this->assertEquals(4, $list->getCount()); + + $this->component->detachEventHandler('OnMyEvent','foopre', 5); + $this->assertEquals(3, $list->getCount()); + + + // Now do detaching of behavior on events + try { + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) { + } + $this->component->attachBehavior('BehaviorTestBehavior', new BehaviorTestBehavior); + + $this->component->attachEventHandler('onBehaviorEvent','foo'); + $this->assertEquals(1, $this->component->getEventHandlers('onBehaviorEvent')->getCount()); + + $this->component->attachEventHandler('onBehaviorEvent','foopre', 5); + $this->component->attachEventHandler('onBehaviorEvent','foopost', 15); + $this->component->attachEventHandler('onBehaviorEvent','foobar', 10); + $this->component->attachEventHandler('onBehaviorEvent','foobarfoobar', 10); + + + + $this->component->detachEventHandler('onBehaviorEvent','foo'); + $list = $this->component->getEventHandlers('onBehaviorEvent'); + $this->assertEquals(4, $list->getCount()); + + $this->assertEquals('foopre', $list[0]); + $this->assertEquals('foobar', $list[1]); + $this->assertEquals('foobarfoobar', $list[2]); + $this->assertEquals('foopost', $list[3]); + + $this->component->detachEventHandler('onBehaviorEvent','foopre', null); + $this->assertEquals(4, $list->getCount()); + + $this->component->detachEventHandler('onBehaviorEvent','foopre', 5); + $this->assertEquals(3, $list->getCount()); + } + + + + public function testRaiseEvent() { $this->component->attachEventHandler('OnMyEvent',array($this->component,'myEventHandler')); $this->assertFalse($this->component->isEventHandled()); @@ -151,7 +1694,210 @@ class TComponentTest extends PHPUnit_Framework_TestCase { $this->assertFalse($this->component->Object->isEventHandled()); $this->component->raiseEvent('OnMyEvent',$this,null); $this->assertTrue($this->component->Object->isEventHandled()); + + $this->component->resetEventHandled(); + $this->component->Object->resetEventHandled(); + + + // Test a behavior on event + $this->component->attachBehavior('test', new BehaviorTestBehavior); + + $this->component->attachEventHandler('onBehaviorEvent',array($this->component,'myEventHandler')); + $this->assertFalse($this->component->isEventHandled()); + $this->component->raiseEvent('onBehaviorEvent',$this,null); + $this->assertTrue($this->component->isEventHandled()); + $this->component->attachEventHandler('onBehaviorEvent',array($this->component,'Object.myEventHandler')); + $this->assertFalse($this->component->Object->isEventHandled()); + $this->component->raiseEvent('onBehaviorEvent',$this,null); + $this->assertTrue($this->component->Object->isEventHandled()); + + //test behavior enabled/disabled events + $this->component->disableBehavior('test'); + + $this->component->resetEventHandled(); + $this->component->Object->resetEventHandled(); + + try { + $this->component->attachEventHandler('onBehaviorEvent',array($this->component,'myEventHandler')); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) {} + $this->assertFalse($this->component->isEventHandled()); + try { + $this->component->raiseEvent('onBehaviorEvent',$this,null); + $this->fail('exception not raised when getting event handlers for undefined event'); + } catch(TInvalidOperationException $e) {} + $this->assertFalse($this->component->isEventHandled()); + + $this->component->enableBehavior('test'); + + + + //Test the return types of this function + + $this->assertFalse($this->component->isEventHandled()); + $this->assertFalse($this->component->Object->isEventHandled()); + $this->assertEquals(array(), $this->component->onBehaviorEvent($this,$this->component)); + $this->assertTrue($this->component->isEventHandled()); + $this->assertTrue($this->component->Object->isEventHandled()); + + // This accumulates all the responses from each of the events + $arr=$this->component->onBehaviorEvent($this, $this->component, TEventResults::EVENT_RESULT_ALL); + $this->assertEquals($this, $arr[0]['sender']); + $this->assertEquals($this->component, $arr[0]['param']); + $this->assertTrue(null === $arr[0]['response']); + + $this->assertEquals($this, $arr[1]['sender']); + $this->assertEquals($this->component, $arr[1]['param']); + $this->assertTrue(null === $arr[1]['response']); + + $this->assertEquals(2, count($arr)); + + // This tests without the default filtering-out of null + $arr=$this->component->onBehaviorEvent($this, $this->component, false); + $this->assertEquals(array(null, null), $arr); + + + unset($this->component->onBehaviorEvent); + $this->assertEquals(0, $this->component->onBehaviorEvent->Count); + + $this->component->onBehaviorEvent = array($this, 'returnValue4'); + $this->component->onBehaviorEvent = array($this, 'returnValue1'); + + // Test the per event post processing function + $arr=$this->component->onBehaviorEvent($this, $this->component, array($this, 'postEventFunction')); + $this->assertEquals(array(exp(4), exp(1)), $arr); + $arr=$this->component->onBehaviorEvent($this, $this->component, array($this, 'postEventFunction2')); + $this->assertEquals(array(sin(4), sin(1)), $arr); + + + //Testing Feed-forward functionality + unset($this->component->onBehaviorEvent); + + $this->component->onBehaviorEvent = array($this, 'ffValue4'); + $this->component->onBehaviorEvent = array($this, 'ffValue2'); + $arr=$this->component->onBehaviorEvent($this, 5, TEventResults::EVENT_RESULT_FEED_FORWARD); + $this->assertEquals(array(20, 40), $arr); + + + unset($this->component->onBehaviorEvent); + + //Order of these events affects the response order in feed forward + $this->component->onBehaviorEvent = array($this, 'ffValue2'); + $this->component->onBehaviorEvent = array($this, 'ffValue4'); + $arr=$this->component->onBehaviorEvent($this, 5, TEventResults::EVENT_RESULT_FEED_FORWARD); + $this->assertEquals(array(10, 40), $arr); + } + + public function returnValue1(){return 1;} + public function returnValue4(){return 4;} + public function postEventFunction($sender, $param, $caller, $response){return exp($response);} + public function postEventFunction2($sender, $param, $caller, $response){return sin($response);} + public function ffValue2($sender, $param){return $param*2;} + public function ffValue4($sender, $param){return $param*4;} + + + public function testGlobalEventListenerInRaiseEvent() { + //TODO Test the Global Event Listener } + + + public function testIDynamicMethodsOnBehavior() { + + //Add Behavior with dynamic call + $this->component->attachBehavior('TDynamicBehavior', new TDynamicBehavior); + + //Check that the behavior is working as it should + $this->assertTrue($this->component->isa('TDynamicBehavior')); + $this->assertNull($this->component->getLastBehaviorDynamicMethodCalled()); + + // call basic behavior implemented method from object (containing behavior) + $this->assertEquals(42, $this->component->TestBehaviorMethod(6, 7)); + + //Test out undefined behavior/host object method + try { + $this->component->objectAndBehaviorUndefinedMethod(); + $this->fail('exception not raised when evaluating an undefined method by the object and behavior'); + } catch(TApplicationException $e) { + } + + // calling undefined dynamic method, caught by the __dycall method in the behavior and implemented + // this behavior catches undefined dynamic event and divides param1 by param 2 + $this->assertEquals(22, $this->component->dyTestDynamicBehaviorMethod(242, 11)); + $this->assertEquals('dyTestDynamicBehaviorMethod', $this->component->getLastBehaviorDynamicMethodCalled()); + + // calling undefined dynamic method, caught by the __dycall in the behavior and ignored + $this->assertNull($this->component->dyUndefinedIntraEvent(242, 11)); + $this->assertEquals('dyUndefinedIntraEvent', $this->component->getLastBehaviorDynamicMethodCalled()); + + //call behavior defined dynamic event + // param1 * 2 * param2 + $this->assertEquals(2420, $this->component->dyTestIntraEvent(121, 10)); + + $this->component->detachBehavior('TDynamicBehavior'); + $this->assertFalse($this->component->isa('TDynamicBehavior')); + + + + //Add Class Behavior with dynamic call + $this->component->attachBehavior('TDynamicClassBehavior', new TDynamicClassBehavior); + + //Check that the behavior is working as it should + $this->assertTrue($this->component->isa('TDynamicClassBehavior')); + $this->assertNull($this->component->getLastBehaviorDynamicMethodCalled()); + + // call basic behavior implemented method from object (containing behavior) + $this->assertEquals(42, $this->component->TestBehaviorMethod(6, 7)); + + //Test out undefined behavior/host object method + try { + $this->component->objectAndBehaviorUndefinedMethod(); + $this->fail('exception not raised when evaluating an undefined method by the object and behavior'); + } catch(TApplicationException $e) { + } + + // calling undefined dynamic method, caught by the __dycall method in the behavior and implemented + // this behavior catches undefined dynamic event and divides param1 by param 2 + $this->assertEquals(22, $this->component->dyTestDynamicClassBehaviorMethod(242, 11)); + $this->assertEquals('dyTestDynamicClassBehaviorMethod', $this->component->getLastBehaviorDynamicMethodCalled()); + + // calling undefined dynamic method, caught by the __dycall in the behavior and ignored + $this->assertNull($this->component->dyUndefinedIntraEvent(242, 11)); + $this->assertEquals('dyUndefinedIntraEvent', $this->component->getLastBehaviorDynamicMethodCalled()); + + //call behavior defined dynamic event + // param1 * 2 * param2 + $this->assertEquals(2420, $this->component->dyTestIntraEvent(121, 10)); + + $this->component->detachBehavior('TDynamicClassBehavior'); + $this->assertFalse($this->component->isa('TDynamicClassBehavior')); + + + } + + // This also tests the priority of the common global raiseEvent events + public function testIDynamicMethodsOnBehaviorGlobalEvents() { + $component = new GlobalRaiseComponent(); + + // common function has a default priority of 10 + $component->attachEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER, array($component, 'commonRaiseEventListener')); + $component->attachEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER, array($component, 'postglobalRaiseEventListener'), 1); + $component->attachEventHandler(TComponent::GLOBAL_RAISE_EVENT_LISTENER, array($component, 'preglobalRaiseEventListener'), -1); + + $this->assertEquals(5, $this->component->fxGlobalListener->getCount()); + $this->assertEquals(1, $this->component->fxPrimaryGlobalEvent->getCount()); + $this->assertEquals(1, $this->component->fxPrimaryGlobalEvent->getCount(), 'fxPrimaryGlobalEvent is not installed on test object'); + + // call the global event on a different object than the test object + $res = $this->component->raiseEvent('fxPrimaryGlobalEvent', $this, null, TEventResults::EVENT_RESULT_ALL); + + $this->assertEquals(6, count($res)); + $this->assertEquals(array('pregl', 'primary', 'postgl', 'fxGL', 'fxcall', 'com'), $component->getCallOrders()); + + $component->unlisten(); + } + + + public function testEvaluateExpression() { $expression="1+2"; @@ -163,6 +1909,9 @@ class TComponentTest extends PHPUnit_Framework_TestCase { } } + + + public function testEvaluateStatements() { $statements='$a="test string"; echo $a;'; $this->assertEquals('test string',$this->component->evaluateStatements($statements)); @@ -173,6 +1922,169 @@ class TComponentTest extends PHPUnit_Framework_TestCase { } catch(Exception $e) { } } + + + public function testDynamicFunctionCall() { + + $this->assertEquals(' aa bb cc __ .. ++ || !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + + $this->component->attachBehavior('dy1', new dy1TextReplace); + $this->assertFalse($this->component->dy1->isCalled()); + $this->assertEquals(' aa bb cc __ __ ++ || !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy1->isCalled()); + + $this->component->attachBehavior('dy2', new dy2TextReplace); + $this->assertFalse($this->component->dy2->isCalled()); + $this->assertEquals(' aa bb cc __ __ || || !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy2->isCalled()); + + $this->component->attachBehavior('dy3', new dy3TextReplace); + $this->assertFalse($this->component->dy3->isCalled()); + $this->assertEquals(' aa bb cc __ __ || || ?? ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy3->isCalled()); + + $this->assertEquals(' aa bb cc __ .. ++ || !! ?? ', $this->component->dyUndefinedEvent(' aa bb cc __ .. ++ || !! ?? ')); + + $this->assertEquals(0.25, $this->component->dyPowerFunction(2,2)); + + + $this->component->detachBehavior('dy1'); + $this->component->detachBehavior('dy2'); + $this->component->detachBehavior('dy3'); + + //test class behaviors of dynamic events and the argument list order + + $this->assertEquals(' aa bb cc __ .. ++ || !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + + $this->component->attachBehavior('dy1', new dy1ClassTextReplace); + $this->assertFalse($this->component->dy1->isCalled()); + $this->assertEquals(' aa bb cc .. .. ++ || !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy1->isCalled()); + + $this->component->attachBehavior('dy2', new dy2ClassTextReplace); + $this->assertFalse($this->component->dy2->isCalled()); + $this->assertEquals(' aa bb cc .. .. ++ ++ !! ?? ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy2->isCalled()); + + $this->component->attachBehavior('dy3', new dy3ClassTextReplace); + $this->assertFalse($this->component->dy3->isCalled()); + $this->assertEquals(' aa bb cc .. .. ++ ++ !! ^_^ ', $this->component->dyTextFilter(' aa bb cc __ .. ++ || !! ?? ')); + $this->assertTrue($this->component->dy3->isCalled()); + + $this->assertEquals(' aa bb cc __ .. ++ || !! ?? ', $this->component->dyUndefinedEvent(' aa bb cc __ .. ++ || !! ?? ')); + + $this->assertEquals(0.25, $this->component->dyPowerFunction(2,2)); + + + } + + + + + public function testDynamicIntraObjectEvents() { + + $this->component->attachBehavior('IntraEvents', new IntraObjectExtenderBehavior); + + $this->assertNull($this->component->IntraEvents->LastCall); + + //unlisten first, this object listens upon instantiation. + $this->component->unlisten(); + $this->assertEquals(2, $this->component->IntraEvents->LastCall); + + // ensures that IntraEvents nulls the last call variable when calling this getter + $this->assertNull($this->component->IntraEvents->LastCall); + + //listen next to undo the unlisten + $this->component->listen(); + $this->assertEquals(1, $this->component->IntraEvents->LastCall); + + + $this->assertEquals(3, $this->component->evaluateExpression('1+2')); + $this->assertEquals(7, $this->component->IntraEvents->LastCall); + + $statements='$a="test string"; echo $a;'; + $this->assertEquals('test string', $this->component->evaluateStatements($statements)); + $this->assertEquals(8, $this->component->IntraEvents->LastCall); + + $component2 = new NewComponentNoListen(); + $this->assertNull($this->component->createdOnTemplate($component2)); + $this->assertEquals(9, $this->component->IntraEvents->LastCall); + + $this->assertNull($this->component->addParsedObject($component2)); + $this->assertEquals(10, $this->component->IntraEvents->LastCall); + + + + $behavior = new BarBehavior; + $this->assertEquals($behavior, $this->component->attachBehavior('BarBehavior', $behavior)); + $this->assertEquals(11, $this->component->IntraEvents->LastCall); + + $this->assertNull($this->component->disableBehaviors()); + $this->assertNull($this->component->enableBehaviors()); + $this->assertEquals(27, $this->component->IntraEvents->LastCall); + + $this->assertTrue($this->component->disableBehavior('BarBehavior')); + $this->assertEquals(16, $this->component->IntraEvents->LastCall); + + $this->assertTrue($this->component->enableBehavior('BarBehavior')); + $this->assertEquals(15, $this->component->IntraEvents->LastCall); + + $this->assertEquals($behavior, $this->component->detachBehavior('BarBehavior')); + $this->assertEquals(12, $this->component->IntraEvents->LastCall); + + + $this->component->attachEventHandler('OnMyEvent',array($this->component,'myEventHandler')); + $this->component->raiseEvent('OnMyEvent',$this,null); + + //3 + 4 + 5 + 6 = 18 (the behavior adds these together when each raiseEvent dynamic intra event is called) + $this->assertEquals(18, $this->component->IntraEvents->LastCall); + } + + + + public function testJavascriptGetterSetter() { + + $this->assertFalse(isset($this->component->ColorAttribute)); + $this->assertFalse(isset($this->component->JsColorAttribute)); + + $this->component->ColorAttribute = "('#556677', '#abcdef', 503987)"; + $this->assertEquals("('#556677', '#abcdef', 503987)", $this->component->ColorAttribute); + + $this->assertTrue(isset($this->component->ColorAttribute)); + $this->assertTrue(isset($this->component->JsColorAttribute)); + + $this->component->ColorAttribute = "new Array(1, 2, 3, 4, 5)"; + $this->assertEquals("new Array(1, 2, 3, 4, 5)", $this->component->JsColorAttribute); + + $this->component->JsColorAttribute = "['#112233', '#fedcba', 22009837]"; + $this->assertEquals("['#112233', '#fedcba', 22009837]", $this->component->ColorAttribute); + } + + + public function testJavascriptIssetUnset() { + + $this->component->JsColorAttribute = "['#112233', '#fedcba', 22009837]"; + $this->assertEquals("['#112233', '#fedcba', 22009837]", $this->component->ColorAttribute); + + unset($this->component->ColorAttribute); + + $this->assertFalse(isset($this->component->ColorAttribute)); + $this->assertFalse(isset($this->component->JsColorAttribute)); + + $this->component->JsColorAttribute = "['#112233', '#fedcba', 22009837]"; + + $this->assertTrue(isset($this->component->ColorAttribute)); + $this->assertTrue(isset($this->component->JsColorAttribute)); + + unset($this->component->JsColorAttribute); + + $this->assertFalse(isset($this->component->ColorAttribute)); + $this->assertFalse(isset($this->component->JsColorAttribute)); + + } + + + } ?>
\ No newline at end of file |