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