Component.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\base;
  8. use Yii;
  9. /**
  10. * Component is the base class that implements the *property*, *event* and *behavior* features.
  11. *
  12. * @include @yii/base/Component.md
  13. *
  14. * @property Behavior[] $behaviors List of behaviors attached to this component. This property is read-only.
  15. *
  16. * @author Qiang Xue <[email protected]>
  17. * @since 2.0
  18. */
  19. class Component extends Object
  20. {
  21. /**
  22. * @var array the attached event handlers (event name => handlers)
  23. */
  24. private $_events = [];
  25. /**
  26. * @var Behavior[] the attached behaviors (behavior name => behavior)
  27. */
  28. private $_behaviors;
  29. /**
  30. * Returns the value of a component property.
  31. * This method will check in the following order and act accordingly:
  32. *
  33. * - a property defined by a getter: return the getter result
  34. * - a property of a behavior: return the behavior property value
  35. *
  36. * Do not call this method directly as it is a PHP magic method that
  37. * will be implicitly called when executing `$value = $component->property;`.
  38. * @param string $name the property name
  39. * @return mixed the property value or the value of a behavior's property
  40. * @throws UnknownPropertyException if the property is not defined
  41. * @throws InvalidCallException if the property is write-only.
  42. * @see __set()
  43. */
  44. public function __get($name)
  45. {
  46. $getter = 'get' . $name;
  47. if (method_exists($this, $getter)) {
  48. // read property, e.g. getName()
  49. return $this->$getter();
  50. } else {
  51. // behavior property
  52. $this->ensureBehaviors();
  53. foreach ($this->_behaviors as $behavior) {
  54. if ($behavior->canGetProperty($name)) {
  55. return $behavior->$name;
  56. }
  57. }
  58. }
  59. if (method_exists($this, 'set' . $name)) {
  60. throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
  61. } else {
  62. throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
  63. }
  64. }
  65. /**
  66. * Sets the value of a component property.
  67. * This method will check in the following order and act accordingly:
  68. *
  69. * - a property defined by a setter: set the property value
  70. * - an event in the format of "on xyz": attach the handler to the event "xyz"
  71. * - a behavior in the format of "as xyz": attach the behavior named as "xyz"
  72. * - a property of a behavior: set the behavior property value
  73. *
  74. * Do not call this method directly as it is a PHP magic method that
  75. * will be implicitly called when executing `$component->property = $value;`.
  76. * @param string $name the property name or the event name
  77. * @param mixed $value the property value
  78. * @throws UnknownPropertyException if the property is not defined
  79. * @throws InvalidCallException if the property is read-only.
  80. * @see __get()
  81. */
  82. public function __set($name, $value)
  83. {
  84. $setter = 'set' . $name;
  85. if (method_exists($this, $setter)) {
  86. // set property
  87. $this->$setter($value);
  88. return;
  89. } elseif (strncmp($name, 'on ', 3) === 0) {
  90. // on event: attach event handler
  91. $this->on(trim(substr($name, 3)), $value);
  92. return;
  93. } elseif (strncmp($name, 'as ', 3) === 0) {
  94. // as behavior: attach behavior
  95. $name = trim(substr($name, 3));
  96. $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
  97. return;
  98. } else {
  99. // behavior property
  100. $this->ensureBehaviors();
  101. foreach ($this->_behaviors as $behavior) {
  102. if ($behavior->canSetProperty($name)) {
  103. $behavior->$name = $value;
  104. return;
  105. }
  106. }
  107. }
  108. if (method_exists($this, 'get' . $name)) {
  109. throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
  110. } else {
  111. throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
  112. }
  113. }
  114. /**
  115. * Checks if a property value is null.
  116. * This method will check in the following order and act accordingly:
  117. *
  118. * - a property defined by a setter: return whether the property value is null
  119. * - a property of a behavior: return whether the property value is null
  120. *
  121. * Do not call this method directly as it is a PHP magic method that
  122. * will be implicitly called when executing `isset($component->property)`.
  123. * @param string $name the property name or the event name
  124. * @return boolean whether the named property is null
  125. */
  126. public function __isset($name)
  127. {
  128. $getter = 'get' . $name;
  129. if (method_exists($this, $getter)) {
  130. return $this->$getter() !== null;
  131. } else {
  132. // behavior property
  133. $this->ensureBehaviors();
  134. foreach ($this->_behaviors as $behavior) {
  135. if ($behavior->canGetProperty($name)) {
  136. return $behavior->$name !== null;
  137. }
  138. }
  139. }
  140. return false;
  141. }
  142. /**
  143. * Sets a component property to be null.
  144. * This method will check in the following order and act accordingly:
  145. *
  146. * - a property defined by a setter: set the property value to be null
  147. * - a property of a behavior: set the property value to be null
  148. *
  149. * Do not call this method directly as it is a PHP magic method that
  150. * will be implicitly called when executing `unset($component->property)`.
  151. * @param string $name the property name
  152. * @throws InvalidCallException if the property is read only.
  153. */
  154. public function __unset($name)
  155. {
  156. $setter = 'set' . $name;
  157. if (method_exists($this, $setter)) {
  158. $this->$setter(null);
  159. return;
  160. } else {
  161. // behavior property
  162. $this->ensureBehaviors();
  163. foreach ($this->_behaviors as $behavior) {
  164. if ($behavior->canSetProperty($name)) {
  165. $behavior->$name = null;
  166. return;
  167. }
  168. }
  169. }
  170. if (method_exists($this, 'get' . $name)) {
  171. throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name);
  172. }
  173. }
  174. /**
  175. * Calls the named method which is not a class method.
  176. *
  177. * This method will check if any attached behavior has
  178. * the named method and will execute it if available.
  179. *
  180. * Do not call this method directly as it is a PHP magic method that
  181. * will be implicitly called when an unknown method is being invoked.
  182. * @param string $name the method name
  183. * @param array $params method parameters
  184. * @return mixed the method return value
  185. * @throws UnknownMethodException when calling unknown method
  186. */
  187. public function __call($name, $params)
  188. {
  189. $this->ensureBehaviors();
  190. foreach ($this->_behaviors as $object) {
  191. if ($object->hasMethod($name)) {
  192. return call_user_func_array([$object, $name], $params);
  193. }
  194. }
  195. throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
  196. }
  197. /**
  198. * This method is called after the object is created by cloning an existing one.
  199. * It removes all behaviors because they are attached to the old object.
  200. */
  201. public function __clone()
  202. {
  203. $this->_events = [];
  204. $this->_behaviors = null;
  205. }
  206. /**
  207. * Returns a value indicating whether a property is defined for this component.
  208. * A property is defined if:
  209. *
  210. * - the class has a getter or setter method associated with the specified name
  211. * (in this case, property name is case-insensitive);
  212. * - the class has a member variable with the specified name (when `$checkVars` is true);
  213. * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
  214. *
  215. * @param string $name the property name
  216. * @param boolean $checkVars whether to treat member variables as properties
  217. * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
  218. * @return boolean whether the property is defined
  219. * @see canGetProperty()
  220. * @see canSetProperty()
  221. */
  222. public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
  223. {
  224. return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors);
  225. }
  226. /**
  227. * Returns a value indicating whether a property can be read.
  228. * A property can be read if:
  229. *
  230. * - the class has a getter method associated with the specified name
  231. * (in this case, property name is case-insensitive);
  232. * - the class has a member variable with the specified name (when `$checkVars` is true);
  233. * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true).
  234. *
  235. * @param string $name the property name
  236. * @param boolean $checkVars whether to treat member variables as properties
  237. * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
  238. * @return boolean whether the property can be read
  239. * @see canSetProperty()
  240. */
  241. public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
  242. {
  243. if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) {
  244. return true;
  245. } elseif ($checkBehaviors) {
  246. $this->ensureBehaviors();
  247. foreach ($this->_behaviors as $behavior) {
  248. if ($behavior->canGetProperty($name, $checkVars)) {
  249. return true;
  250. }
  251. }
  252. }
  253. return false;
  254. }
  255. /**
  256. * Returns a value indicating whether a property can be set.
  257. * A property can be written if:
  258. *
  259. * - the class has a setter method associated with the specified name
  260. * (in this case, property name is case-insensitive);
  261. * - the class has a member variable with the specified name (when `$checkVars` is true);
  262. * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true).
  263. *
  264. * @param string $name the property name
  265. * @param boolean $checkVars whether to treat member variables as properties
  266. * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
  267. * @return boolean whether the property can be written
  268. * @see canGetProperty()
  269. */
  270. public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
  271. {
  272. if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
  273. return true;
  274. } elseif ($checkBehaviors) {
  275. $this->ensureBehaviors();
  276. foreach ($this->_behaviors as $behavior) {
  277. if ($behavior->canSetProperty($name, $checkVars)) {
  278. return true;
  279. }
  280. }
  281. }
  282. return false;
  283. }
  284. /**
  285. * Returns a value indicating whether a method is defined.
  286. * A method is defined if:
  287. *
  288. * - the class has a method with the specified name
  289. * - an attached behavior has a method with the given name (when `$checkBehaviors` is true).
  290. *
  291. * @param string $name the property name
  292. * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component
  293. * @return boolean whether the property is defined
  294. */
  295. public function hasMethod($name, $checkBehaviors = true)
  296. {
  297. if (method_exists($this, $name)) {
  298. return true;
  299. } elseif ($checkBehaviors) {
  300. $this->ensureBehaviors();
  301. foreach ($this->_behaviors as $behavior) {
  302. if ($behavior->hasMethod($name)) {
  303. return true;
  304. }
  305. }
  306. }
  307. return false;
  308. }
  309. /**
  310. * Returns a list of behaviors that this component should behave as.
  311. *
  312. * Child classes may override this method to specify the behaviors they want to behave as.
  313. *
  314. * The return value of this method should be an array of behavior objects or configurations
  315. * indexed by behavior names. A behavior configuration can be either a string specifying
  316. * the behavior class or an array of the following structure:
  317. *
  318. * ~~~
  319. * 'behaviorName' => [
  320. * 'class' => 'BehaviorClass',
  321. * 'property1' => 'value1',
  322. * 'property2' => 'value2',
  323. * ]
  324. * ~~~
  325. *
  326. * Note that a behavior class must extend from [[Behavior]]. Behavior names can be strings
  327. * or integers. If the former, they uniquely identify the behaviors. If the latter, the corresponding
  328. * behaviors are anonymous and their properties and methods will NOT be made available via the component
  329. * (however, the behaviors can still respond to the component's events).
  330. *
  331. * Behaviors declared in this method will be attached to the component automatically (on demand).
  332. *
  333. * @return array the behavior configurations.
  334. */
  335. public function behaviors()
  336. {
  337. return [];
  338. }
  339. /**
  340. * Returns a value indicating whether there is any handler attached to the named event.
  341. * @param string $name the event name
  342. * @return boolean whether there is any handler attached to the event.
  343. */
  344. public function hasEventHandlers($name)
  345. {
  346. $this->ensureBehaviors();
  347. return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
  348. }
  349. /**
  350. * Attaches an event handler to an event.
  351. *
  352. * The event handler must be a valid PHP callback. The followings are
  353. * some examples:
  354. *
  355. * ~~~
  356. * function ($event) { ... } // anonymous function
  357. * [$object, 'handleClick'] // $object->handleClick()
  358. * ['Page', 'handleClick'] // Page::handleClick()
  359. * 'handleClick' // global function handleClick()
  360. * ~~~
  361. *
  362. * The event handler must be defined with the following signature,
  363. *
  364. * ~~~
  365. * function ($event)
  366. * ~~~
  367. *
  368. * where `$event` is an [[Event]] object which includes parameters associated with the event.
  369. *
  370. * @param string $name the event name
  371. * @param callback $handler the event handler
  372. * @param mixed $data the data to be passed to the event handler when the event is triggered.
  373. * When the event handler is invoked, this data can be accessed via [[Event::data]].
  374. * @see off()
  375. */
  376. public function on($name, $handler, $data = null)
  377. {
  378. $this->ensureBehaviors();
  379. $this->_events[$name][] = [$handler, $data];
  380. }
  381. /**
  382. * Detaches an existing event handler from this component.
  383. * This method is the opposite of [[on()]].
  384. * @param string $name event name
  385. * @param callback $handler the event handler to be removed.
  386. * If it is null, all handlers attached to the named event will be removed.
  387. * @return boolean if a handler is found and detached
  388. * @see on()
  389. */
  390. public function off($name, $handler = null)
  391. {
  392. $this->ensureBehaviors();
  393. if (empty($this->_events[$name])) {
  394. return false;
  395. }
  396. if ($handler === null) {
  397. unset($this->_events[$name]);
  398. return true;
  399. } else {
  400. $removed = false;
  401. foreach ($this->_events[$name] as $i => $event) {
  402. if ($event[0] === $handler) {
  403. unset($this->_events[$name][$i]);
  404. $removed = true;
  405. }
  406. }
  407. if ($removed) {
  408. $this->_events[$name] = array_values($this->_events[$name]);
  409. }
  410. return $removed;
  411. }
  412. }
  413. /**
  414. * Triggers an event.
  415. * This method represents the happening of an event. It invokes
  416. * all attached handlers for the event including class-level handlers.
  417. * @param string $name the event name
  418. * @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
  419. */
  420. public function trigger($name, Event $event = null)
  421. {
  422. $this->ensureBehaviors();
  423. if (!empty($this->_events[$name])) {
  424. if ($event === null) {
  425. $event = new Event;
  426. }
  427. if ($event->sender === null) {
  428. $event->sender = $this;
  429. }
  430. $event->handled = false;
  431. $event->name = $name;
  432. foreach ($this->_events[$name] as $handler) {
  433. $event->data = $handler[1];
  434. call_user_func($handler[0], $event);
  435. // stop further handling if the event is handled
  436. if ($event->handled) {
  437. return;
  438. }
  439. }
  440. }
  441. // invoke class-level attached handlers
  442. Event::trigger($this, $name, $event);
  443. }
  444. /**
  445. * Returns the named behavior object.
  446. * @param string $name the behavior name
  447. * @return Behavior the behavior object, or null if the behavior does not exist
  448. */
  449. public function getBehavior($name)
  450. {
  451. $this->ensureBehaviors();
  452. return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
  453. }
  454. /**
  455. * Returns all behaviors attached to this component.
  456. * @return Behavior[] list of behaviors attached to this component
  457. */
  458. public function getBehaviors()
  459. {
  460. $this->ensureBehaviors();
  461. return $this->_behaviors;
  462. }
  463. /**
  464. * Attaches a behavior to this component.
  465. * This method will create the behavior object based on the given
  466. * configuration. After that, the behavior object will be attached to
  467. * this component by calling the [[Behavior::attach()]] method.
  468. * @param string $name the name of the behavior.
  469. * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following:
  470. *
  471. * - a [[Behavior]] object
  472. * - a string specifying the behavior class
  473. * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
  474. *
  475. * @return Behavior the behavior object
  476. * @see detachBehavior()
  477. */
  478. public function attachBehavior($name, $behavior)
  479. {
  480. $this->ensureBehaviors();
  481. return $this->attachBehaviorInternal($name, $behavior);
  482. }
  483. /**
  484. * Attaches a list of behaviors to the component.
  485. * Each behavior is indexed by its name and should be a [[Behavior]] object,
  486. * a string specifying the behavior class, or an configuration array for creating the behavior.
  487. * @param array $behaviors list of behaviors to be attached to the component
  488. * @see attachBehavior()
  489. */
  490. public function attachBehaviors($behaviors)
  491. {
  492. $this->ensureBehaviors();
  493. foreach ($behaviors as $name => $behavior) {
  494. $this->attachBehaviorInternal($name, $behavior);
  495. }
  496. }
  497. /**
  498. * Detaches a behavior from the component.
  499. * The behavior's [[Behavior::detach()]] method will be invoked.
  500. * @param string $name the behavior's name.
  501. * @return Behavior the detached behavior. Null if the behavior does not exist.
  502. */
  503. public function detachBehavior($name)
  504. {
  505. $this->ensureBehaviors();
  506. if (isset($this->_behaviors[$name])) {
  507. $behavior = $this->_behaviors[$name];
  508. unset($this->_behaviors[$name]);
  509. $behavior->detach();
  510. return $behavior;
  511. } else {
  512. return null;
  513. }
  514. }
  515. /**
  516. * Detaches all behaviors from the component.
  517. */
  518. public function detachBehaviors()
  519. {
  520. $this->ensureBehaviors();
  521. foreach ($this->_behaviors as $name => $behavior) {
  522. $this->detachBehavior($name);
  523. }
  524. }
  525. /**
  526. * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component.
  527. */
  528. public function ensureBehaviors()
  529. {
  530. if ($this->_behaviors === null) {
  531. $this->_behaviors = [];
  532. foreach ($this->behaviors() as $name => $behavior) {
  533. $this->attachBehaviorInternal($name, $behavior);
  534. }
  535. }
  536. }
  537. /**
  538. * Attaches a behavior to this component.
  539. * @param string $name the name of the behavior.
  540. * @param string|array|Behavior $behavior the behavior to be attached
  541. * @return Behavior the attached behavior.
  542. */
  543. private function attachBehaviorInternal($name, $behavior)
  544. {
  545. if (!($behavior instanceof Behavior)) {
  546. $behavior = Yii::createObject($behavior);
  547. }
  548. if (isset($this->_behaviors[$name])) {
  549. $this->_behaviors[$name]->detach();
  550. }
  551. $behavior->attach($this);
  552. return $this->_behaviors[$name] = $behavior;
  553. }
  554. }