* @since 2.0 */ class Component extends Object { /** * @var array the attached event handlers (event name => handlers) */ private $_events; /** * @var Behavior[] the attached behaviors (behavior name => behavior) */ private $_behaviors; /** * Returns the value of a component property. * This method will check in the following order and act accordingly: * * - a property defined by a getter: return the getter result * - a property of a behavior: return the behavior property value * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `$value = $component->property;`. * @param string $name the property name * @return mixed the property value or the value of a behavior's property * @throws UnknownPropertyException if the property is not defined * @throws InvalidCallException if the property is write-only. * @see __set() */ public function __get($name) { $getter = 'get' . $name; if (method_exists($this, $getter)) { // read property, e.g. getName() return $this->$getter(); } else { // behavior property $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name; } } } if (method_exists($this, 'set' . $name)) { throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); } else { throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } /** * Sets the value of a component property. * This method will check in the following order and act accordingly: * * - a property defined by a setter: set the property value * - an event in the format of "on xyz": attach the handler to the event "xyz" * - a behavior in the format of "as xyz": attach the behavior named as "xyz" * - a property of a behavior: set the behavior property value * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `$component->property = $value;`. * @param string $name the property name or the event name * @param mixed $value the property value * @throws UnknownPropertyException if the property is not defined * @throws InvalidCallException if the property is read-only. * @see __get() */ public function __set($name, $value) { $setter = 'set' . $name; if (method_exists($this, $setter)) { // set property $this->$setter($value); return; } elseif (strncmp($name, 'on ', 3) === 0) { // on event: attach event handler $this->on(trim(substr($name, 3)), $value); return; } elseif (strncmp($name, 'as ', 3) === 0) { // as behavior: attach behavior $name = trim(substr($name, 3)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); return; } else { // behavior property $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = $value; return; } } } if (method_exists($this, 'get' . $name)) { throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } /** * Checks if a property value is null. * This method will check in the following order and act accordingly: * * - a property defined by a setter: return whether the property value is null * - a property of a behavior: return whether the property value is null * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `isset($component->property)`. * @param string $name the property name or the event name * @return boolean whether the named property is null */ public function __isset($name) { $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter() !== null; } else { // behavior property $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name !== null; } } } return false; } /** * Sets a component property to be null. * This method will check in the following order and act accordingly: * * - a property defined by a setter: set the property value to be null * - a property of a behavior: set the property value to be null * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `unset($component->property)`. * @param string $name the property name * @throws InvalidCallException if the property is read only. */ public function __unset($name) { $setter = 'set' . $name; if (method_exists($this, $setter)) { $this->$setter(null); return; } else { // behavior property $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = null; return; } } } if (method_exists($this, 'get' . $name)) { throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name); } } /** * Calls the named method which is not a class method. * * This method will check if any attached behavior has * the named method and will execute it if available. * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when an unknown method is being invoked. * @param string $name the method name * @param array $params method parameters * @return mixed the method return value * @throws UnknownMethodException when calling unknown method */ public function __call($name, $params) { $this->ensureBehaviors(); foreach ($this->_behaviors as $object) { if ($object->hasMethod($name)) { return call_user_func_array([$object, $name], $params); } } throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()"); } /** * This method is called after the object is created by cloning an existing one. * It removes all behaviors because they are attached to the old object. */ public function __clone() { $this->_events = null; $this->_behaviors = null; } /** * Returns a value indicating whether a property is defined for this component. * A property is defined if: * * - the class has a getter or setter method associated with the specified name * (in this case, property name is case-insensitive); * - the class has a member variable with the specified name (when `$checkVars` is true); * - an attached behavior has a property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name * @param boolean $checkVars whether to treat member variables as properties * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property is defined * @see canGetProperty() * @see canSetProperty() */ public function hasProperty($name, $checkVars = true, $checkBehaviors = true) { return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors); } /** * Returns a value indicating whether a property can be read. * A property can be read if: * * - the class has a getter method associated with the specified name * (in this case, property name is case-insensitive); * - the class has a member variable with the specified name (when `$checkVars` is true); * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name * @param boolean $checkVars whether to treat member variables as properties * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property can be read * @see canSetProperty() */ public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) { if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) { return true; } elseif ($checkBehaviors) { $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name, $checkVars)) { return true; } } } return false; } /** * Returns a value indicating whether a property can be set. * A property can be written if: * * - the class has a setter method associated with the specified name * (in this case, property name is case-insensitive); * - the class has a member variable with the specified name (when `$checkVars` is true); * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true). * * @param string $name the property name * @param boolean $checkVars whether to treat member variables as properties * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component * @return boolean whether the property can be written * @see canGetProperty() */ public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) { return true; } elseif ($checkBehaviors) { $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name, $checkVars)) { return true; } } } return false; } /** * Returns a value indicating whether a method is defined. * A method is defined if: * * - the class has a method with the specified name * - an attached behavior has a method with the given name (when `$checkBehaviors` is true). * * @param string $name the property name * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component * @return boolean whether the property is defined */ public function hasMethod($name, $checkBehaviors = true) { if (method_exists($this, $name)) { return true; } elseif ($checkBehaviors) { $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->hasMethod($name)) { return true; } } } return false; } /** * Returns a list of behaviors that this component should behave as. * * Child classes may override this method to specify the behaviors they want to behave as. * * The return value of this method should be an array of behavior objects or configurations * indexed by behavior names. A behavior configuration can be either a string specifying * the behavior class or an array of the following structure: * * ~~~ * 'behaviorName' => [ * 'class' => 'BehaviorClass', * 'property1' => 'value1', * 'property2' => 'value2', * ] * ~~~ * * Note that a behavior class must extend from [[Behavior]]. Behavior names can be strings * or integers. If the former, they uniquely identify the behaviors. If the latter, the corresponding * behaviors are anonymous and their properties and methods will NOT be made available via the component * (however, the behaviors can still respond to the component's events). * * Behaviors declared in this method will be attached to the component automatically (on demand). * * @return array the behavior configurations. */ public function behaviors() { return []; } /** * Returns a value indicating whether there is any handler attached to the named event. * @param string $name the event name * @return boolean whether there is any handler attached to the event. */ public function hasEventHandlers($name) { $this->ensureBehaviors(); return !empty($this->_events[$name]) || Event::hasHandlers($this, $name); } /** * Attaches an event handler to an event. * * The event handler must be a valid PHP callback. The followings are * some examples: * * ~~~ * function ($event) { ... } // anonymous function * [$object, 'handleClick'] // $object->handleClick() * ['Page', 'handleClick'] // Page::handleClick() * 'handleClick' // global function handleClick() * ~~~ * * The event handler must be defined with the following signature, * * ~~~ * function ($event) * ~~~ * * where `$event` is an [[Event]] object which includes parameters associated with the event. * * @param string $name the event name * @param callback $handler the event handler * @param mixed $data the data to be passed to the event handler when the event is triggered. * When the event handler is invoked, this data can be accessed via [[Event::data]]. * @see off() */ public function on($name, $handler, $data = null) { $this->ensureBehaviors(); $this->_events[$name][] = [$handler, $data]; } /** * Detaches an existing event handler from this component. * This method is the opposite of [[on()]]. * @param string $name event name * @param callback $handler the event handler to be removed. * If it is null, all handlers attached to the named event will be removed. * @return boolean if a handler is found and detached * @see on() */ public function off($name, $handler = null) { $this->ensureBehaviors(); if (empty($this->_events[$name])) { return false; } if ($handler === null) { unset($this->_events[$name]); return true; } else { $removed = false; foreach ($this->_events[$name] as $i => $event) { if ($event[0] === $handler) { unset($this->_events[$name][$i]); $removed = true; } } if ($removed) { $this->_events[$name] = array_values($this->_events[$name]); } return $removed; } } /** * Triggers an event. * This method represents the happening of an event. It invokes * all attached handlers for the event including 'class-level' handlers * @param string $name the event name * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. */ public function trigger($name, Event $event = null) { $this->ensureBehaviors(); if (!empty($this->_events[$name])) { if ($event === null) { $event = new Event; } if ($event->sender === null) { $event->sender = $this; } $event->handled = false; $event->name = $name; foreach ($this->_events[$name] as $handler) { $event->data = $handler[1]; call_user_func($handler[0], $event); // stop further handling if the event is handled if ($event->handled) { return; } } } //invoke 'class-level' attached handlers Event::trigger($this, $name, $event); } /** * Returns the named behavior object. * @param string $name the behavior name * @return Behavior the behavior object, or null if the behavior does not exist */ public function getBehavior($name) { $this->ensureBehaviors(); return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; } /** * Returns all behaviors attached to this component. * @return Behavior[] list of behaviors attached to this component */ public function getBehaviors() { $this->ensureBehaviors(); return $this->_behaviors; } /** * 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 attached to * this component by calling the [[Behavior::attach()]] method. * @param string $name the name of the behavior. * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * * - a [[Behavior]] object * - a string specifying the behavior class * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object. * * @return Behavior the behavior object * @see detachBehavior() */ public function attachBehavior($name, $behavior) { $this->ensureBehaviors(); return $this->attachBehaviorInternal($name, $behavior); } /** * Attaches a list of behaviors to the component. * Each behavior is indexed by its name and should be a [[Behavior]] object, * a string specifying the behavior class, or an configuration array for creating the behavior. * @param array $behaviors list of behaviors to be attached to the component * @see attachBehavior() */ public function attachBehaviors($behaviors) { $this->ensureBehaviors(); foreach ($behaviors as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } } /** * Detaches a behavior from the component. * The behavior's [[Behavior::detach()]] method will be invoked. * @param string $name the behavior's name. * @return Behavior the detached behavior. Null if the behavior does not exist. */ public function detachBehavior($name) { $this->ensureBehaviors(); if (isset($this->_behaviors[$name])) { $behavior = $this->_behaviors[$name]; unset($this->_behaviors[$name]); $behavior->detach(); return $behavior; } else { return null; } } /** * Detaches all behaviors from the component. */ public function detachBehaviors() { $this->ensureBehaviors(); if ($this->_behaviors !== null) { foreach ($this->_behaviors as $name => $behavior) { $this->detachBehavior($name); } } $this->_behaviors = []; } /** * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component. */ public function ensureBehaviors() { if ($this->_behaviors === null) { $this->_behaviors = []; foreach ($this->behaviors() as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } } } /** * Attaches a behavior to this component. * @param string $name the name of the behavior. * @param string|array|Behavior $behavior the behavior to be attached * @return Behavior the attached behavior. */ private function attachBehaviorInternal($name, $behavior) { if (!($behavior instanceof Behavior)) { $behavior = Yii::createObject($behavior); } if (isset($this->_behaviors[$name])) { $this->_behaviors[$name]->detach(); } $behavior->attach($this); return $this->_behaviors[$name] = $behavior; } }