Behavior.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\ORM;
  16. use Cake\Core\Exception\Exception;
  17. use Cake\Core\InstanceConfigTrait;
  18. use Cake\Event\EventListenerInterface;
  19. use ReflectionClass;
  20. use ReflectionMethod;
  21. /**
  22. * Base class for behaviors.
  23. *
  24. * Behaviors allow you to simulate mixins, and create
  25. * reusable blocks of application logic, that can be reused across
  26. * several models. Behaviors also provide a way to hook into model
  27. * callbacks and augment their behavior.
  28. *
  29. * ### Mixin methods
  30. *
  31. * Behaviors can provide mixin like features by declaring public
  32. * methods. These methods will be accessible on the tables the
  33. * behavior has been added to.
  34. *
  35. * ```
  36. * function doSomething($arg1, $arg2) {
  37. * // do something
  38. * }
  39. * ```
  40. *
  41. * Would be called like `$table->doSomething($arg1, $arg2);`.
  42. *
  43. * ### Callback methods
  44. *
  45. * Behaviors can listen to any events fired on a Table. By default
  46. * CakePHP provides a number of lifecycle events your behaviors can
  47. * listen to:
  48. *
  49. * - `beforeFind(Event $event, Query $query, ArrayObject $options, boolean $primary)`
  50. * Fired before each find operation. By stopping the event and supplying a
  51. * return value you can bypass the find operation entirely. Any changes done
  52. * to the $query instance will be retained for the rest of the find. The
  53. * $primary parameter indicates whether or not this is the root query,
  54. * or an associated query.
  55. *
  56. * - `buildValidator(Event $event, Validator $validator, string $name)`
  57. * Fired when the validator object identified by $name is being built. You can use this
  58. * callback to add validation rules or add validation providers.
  59. *
  60. * - `buildRules(Event $event, RulesChecker $rules)`
  61. * Fired when the rules checking object for the table is being built. You can use this
  62. * callback to add more rules to the set.
  63. *
  64. * - `beforeRules(Event $event, EntityInterface $entity, ArrayObject $options, $operation)`
  65. * Fired before an entity is validated using by a rules checker. By stopping this event,
  66. * you can return the final value of the rules checking operation.
  67. *
  68. * - `afterRules(Event $event, EntityInterface $entity, ArrayObject $options, bool $result, $operation)`
  69. * Fired after the rules have been checked on the entity. By stopping this event,
  70. * you can return the final value of the rules checking operation.
  71. *
  72. * - `beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)`
  73. * Fired before each entity is saved. Stopping this event will abort the save
  74. * operation. When the event is stopped the result of the event will be returned.
  75. *
  76. * - `afterSave(Event $event, EntityInterface $entity, ArrayObject $options)`
  77. * Fired after an entity is saved.
  78. *
  79. * - `beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)`
  80. * Fired before an entity is deleted. By stopping this event you will abort
  81. * the delete operation.
  82. *
  83. * - `afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)`
  84. * Fired after an entity has been deleted.
  85. *
  86. * In addition to the core events, behaviors can respond to any
  87. * event fired from your Table classes including custom application
  88. * specific ones.
  89. *
  90. * You can set the priority of a behaviors callbacks by using the
  91. * `priority` setting when attaching a behavior. This will set the
  92. * priority for all the callbacks a behavior provides.
  93. *
  94. * ### Finder methods
  95. *
  96. * Behaviors can provide finder methods that hook into a Table's
  97. * find() method. Custom finders are a great way to provide preset
  98. * queries that relate to your behavior. For example a SluggableBehavior
  99. * could provide a find('slugged') finder. Behavior finders
  100. * are implemented the same as other finders. Any method
  101. * starting with `find` will be setup as a finder. Your finder
  102. * methods should expect the following arguments:
  103. *
  104. * ```
  105. * findSlugged(Query $query, array $options)
  106. * ```
  107. *
  108. * @see \Cake\ORM\Table::addBehavior()
  109. * @see \Cake\Event\EventManager
  110. */
  111. class Behavior implements EventListenerInterface
  112. {
  113. use InstanceConfigTrait;
  114. /**
  115. * Table instance.
  116. *
  117. * @var \Cake\ORM\Table
  118. */
  119. protected $_table;
  120. /**
  121. * Reflection method cache for behaviors.
  122. *
  123. * Stores the reflected method + finder methods per class.
  124. * This prevents reflecting the same class multiple times in a single process.
  125. *
  126. * @var array
  127. */
  128. protected static $_reflectionCache = [];
  129. /**
  130. * Default configuration
  131. *
  132. * These are merged with user-provided configuration when the behavior is used.
  133. *
  134. * @var array
  135. */
  136. protected $_defaultConfig = [];
  137. /**
  138. * Constructor
  139. *
  140. * Merges config with the default and store in the config property
  141. *
  142. * @param \Cake\ORM\Table $table The table this behavior is attached to.
  143. * @param array $config The config for this behavior.
  144. */
  145. public function __construct(Table $table, array $config = [])
  146. {
  147. $config = $this->_resolveMethodAliases(
  148. 'implementedFinders',
  149. $this->_defaultConfig,
  150. $config
  151. );
  152. $config = $this->_resolveMethodAliases(
  153. 'implementedMethods',
  154. $this->_defaultConfig,
  155. $config
  156. );
  157. $this->_table = $table;
  158. $this->config($config);
  159. $this->initialize($config);
  160. }
  161. /**
  162. * Constructor hook method.
  163. *
  164. * Implement this method to avoid having to overwrite
  165. * the constructor and call parent.
  166. *
  167. * @param array $config The configuration settings provided to this behavior.
  168. * @return void
  169. */
  170. public function initialize(array $config)
  171. {
  172. }
  173. /**
  174. * Removes aliased methods that would otherwise be duplicated by userland configuration.
  175. *
  176. * @param string $key The key to filter.
  177. * @param array $defaults The default method mappings.
  178. * @param array $config The customized method mappings.
  179. * @return array A de-duped list of config data.
  180. */
  181. protected function _resolveMethodAliases($key, $defaults, $config)
  182. {
  183. if (!isset($defaults[$key], $config[$key])) {
  184. return $config;
  185. }
  186. if (isset($config[$key]) && $config[$key] === []) {
  187. $this->config($key, [], false);
  188. unset($config[$key]);
  189. return $config;
  190. }
  191. $indexed = array_flip($defaults[$key]);
  192. $indexedCustom = array_flip($config[$key]);
  193. foreach ($indexed as $method => $alias) {
  194. if (!isset($indexedCustom[$method])) {
  195. $indexedCustom[$method] = $alias;
  196. }
  197. }
  198. $this->config($key, array_flip($indexedCustom), false);
  199. unset($config[$key]);
  200. return $config;
  201. }
  202. /**
  203. * verifyConfig
  204. *
  205. * Checks that implemented keys contain values pointing at callable.
  206. *
  207. * @return void
  208. * @throws \Cake\Core\Exception\Exception if config are invalid
  209. */
  210. public function verifyConfig()
  211. {
  212. $keys = ['implementedFinders', 'implementedMethods'];
  213. foreach ($keys as $key) {
  214. if (!isset($this->_config[$key])) {
  215. continue;
  216. }
  217. foreach ($this->_config[$key] as $method) {
  218. if (!is_callable([$this, $method])) {
  219. throw new Exception(sprintf('The method %s is not callable on class %s', $method, get_class($this)));
  220. }
  221. }
  222. }
  223. }
  224. /**
  225. * Gets the Model callbacks this behavior is interested in.
  226. *
  227. * By defining one of the callback methods a behavior is assumed
  228. * to be interested in the related event.
  229. *
  230. * Override this method if you need to add non-conventional event listeners.
  231. * Or if you want your behavior to listen to non-standard events.
  232. *
  233. * @return array
  234. */
  235. public function implementedEvents()
  236. {
  237. $eventMap = [
  238. 'Model.beforeMarshal' => 'beforeMarshal',
  239. 'Model.beforeFind' => 'beforeFind',
  240. 'Model.beforeSave' => 'beforeSave',
  241. 'Model.afterSave' => 'afterSave',
  242. 'Model.afterSaveCommit' => 'afterSaveCommit',
  243. 'Model.beforeDelete' => 'beforeDelete',
  244. 'Model.afterDelete' => 'afterDelete',
  245. 'Model.afterDeleteCommit' => 'afterDeleteCommit',
  246. 'Model.buildValidator' => 'buildValidator',
  247. 'Model.buildRules' => 'buildRules',
  248. 'Model.beforeRules' => 'beforeRules',
  249. 'Model.afterRules' => 'afterRules',
  250. ];
  251. $config = $this->config();
  252. $priority = isset($config['priority']) ? $config['priority'] : null;
  253. $events = [];
  254. foreach ($eventMap as $event => $method) {
  255. if (!method_exists($this, $method)) {
  256. continue;
  257. }
  258. if ($priority === null) {
  259. $events[$event] = $method;
  260. } else {
  261. $events[$event] = [
  262. 'callable' => $method,
  263. 'priority' => $priority
  264. ];
  265. }
  266. }
  267. return $events;
  268. }
  269. /**
  270. * implementedFinders
  271. *
  272. * Provides an alias->methodname map of which finders a behavior implements. Example:
  273. *
  274. * ```
  275. * [
  276. * 'this' => 'findThis',
  277. * 'alias' => 'findMethodName'
  278. * ]
  279. * ```
  280. *
  281. * With the above example, a call to `$Table->find('this')` will call `$Behavior->findThis()`
  282. * and a call to `$Table->find('alias')` will call `$Behavior->findMethodName()`
  283. *
  284. * It is recommended, though not required, to define implementedFinders in the config property
  285. * of child classes such that it is not necessary to use reflections to derive the available
  286. * method list. See core behaviors for examples
  287. *
  288. * @return array
  289. */
  290. public function implementedFinders()
  291. {
  292. $methods = $this->config('implementedFinders');
  293. if (isset($methods)) {
  294. return $methods;
  295. }
  296. return $this->_reflectionCache()['finders'];
  297. }
  298. /**
  299. * implementedMethods
  300. *
  301. * Provides an alias->methodname map of which methods a behavior implements. Example:
  302. *
  303. * ```
  304. * [
  305. * 'method' => 'method',
  306. * 'aliasedmethod' => 'somethingElse'
  307. * ]
  308. * ```
  309. *
  310. * With the above example, a call to `$Table->method()` will call `$Behavior->method()`
  311. * and a call to `$Table->aliasedmethod()` will call `$Behavior->somethingElse()`
  312. *
  313. * It is recommended, though not required, to define implementedFinders in the config property
  314. * of child classes such that it is not necessary to use reflections to derive the available
  315. * method list. See core behaviors for examples
  316. *
  317. * @return array
  318. */
  319. public function implementedMethods()
  320. {
  321. $methods = $this->config('implementedMethods');
  322. if (isset($methods)) {
  323. return $methods;
  324. }
  325. return $this->_reflectionCache()['methods'];
  326. }
  327. /**
  328. * Gets the methods implemented by this behavior
  329. *
  330. * Uses the implementedEvents() method to exclude callback methods.
  331. * Methods starting with `_` will be ignored, as will methods
  332. * declared on Cake\ORM\Behavior
  333. *
  334. * @return array
  335. */
  336. protected function _reflectionCache()
  337. {
  338. $class = get_class($this);
  339. if (isset(self::$_reflectionCache[$class])) {
  340. return self::$_reflectionCache[$class];
  341. }
  342. $events = $this->implementedEvents();
  343. $eventMethods = [];
  344. foreach ($events as $e => $binding) {
  345. if (is_array($binding) && isset($binding['callable'])) {
  346. /* @var string $callable */
  347. $callable = $binding['callable'];
  348. $binding = $callable;
  349. }
  350. $eventMethods[$binding] = true;
  351. }
  352. $baseClass = 'Cake\ORM\Behavior';
  353. if (isset(self::$_reflectionCache[$baseClass])) {
  354. $baseMethods = self::$_reflectionCache[$baseClass];
  355. } else {
  356. $baseMethods = get_class_methods($baseClass);
  357. self::$_reflectionCache[$baseClass] = $baseMethods;
  358. }
  359. $return = [
  360. 'finders' => [],
  361. 'methods' => []
  362. ];
  363. $reflection = new ReflectionClass($class);
  364. foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
  365. $methodName = $method->getName();
  366. if (in_array($methodName, $baseMethods) ||
  367. isset($eventMethods[$methodName])
  368. ) {
  369. continue;
  370. }
  371. if (substr($methodName, 0, 4) === 'find') {
  372. $return['finders'][lcfirst(substr($methodName, 4))] = $methodName;
  373. } else {
  374. $return['methods'][$methodName] = $methodName;
  375. }
  376. }
  377. return self::$_reflectionCache[$class] = $return;
  378. }
  379. }