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: use InstanceConfigTrait;
116:
117: /**
118: * Table instance.
119: *
120: * @var \Cake\ORM\Table
121: */
122: protected $_table;
123:
124: /**
125: * Reflection method cache for behaviors.
126: *
127: * Stores the reflected method + finder methods per class.
128: * This prevents reflecting the same class multiple times in a single process.
129: *
130: * @var array
131: */
132: protected static $_reflectionCache = [];
133:
134: /**
135: * Default configuration
136: *
137: * These are merged with user-provided configuration when the behavior is used.
138: *
139: * @var array
140: */
141: protected $_defaultConfig = [];
142:
143: /**
144: * Constructor
145: *
146: * Merges config with the default and store in the config property
147: *
148: * @param \Cake\ORM\Table $table The table this behavior is attached to.
149: * @param array $config The config for this behavior.
150: */
151: public function __construct(Table $table, array $config = [])
152: {
153: $config = $this->_resolveMethodAliases(
154: 'implementedFinders',
155: $this->_defaultConfig,
156: $config
157: );
158: $config = $this->_resolveMethodAliases(
159: 'implementedMethods',
160: $this->_defaultConfig,
161: $config
162: );
163: $this->_table = $table;
164: $this->setConfig($config);
165: $this->initialize($config);
166: }
167:
168: /**
169: * Constructor hook method.
170: *
171: * Implement this method to avoid having to overwrite
172: * the constructor and call parent.
173: *
174: * @param array $config The configuration settings provided to this behavior.
175: * @return void
176: */
177: public function initialize(array $config)
178: {
179: }
180:
181: /**
182: * Get the table instance this behavior is bound to.
183: *
184: * @return \Cake\ORM\Table The bound table instance.
185: */
186: public function getTable()
187: {
188: return $this->_table;
189: }
190:
191: /**
192: * Removes aliased methods that would otherwise be duplicated by userland configuration.
193: *
194: * @param string $key The key to filter.
195: * @param array $defaults The default method mappings.
196: * @param array $config The customized method mappings.
197: * @return array A de-duped list of config data.
198: */
199: protected function _resolveMethodAliases($key, $defaults, $config)
200: {
201: if (!isset($defaults[$key], $config[$key])) {
202: return $config;
203: }
204: if (isset($config[$key]) && $config[$key] === []) {
205: $this->setConfig($key, [], false);
206: unset($config[$key]);
207:
208: return $config;
209: }
210:
211: $indexed = array_flip($defaults[$key]);
212: $indexedCustom = array_flip($config[$key]);
213: foreach ($indexed as $method => $alias) {
214: if (!isset($indexedCustom[$method])) {
215: $indexedCustom[$method] = $alias;
216: }
217: }
218: $this->setConfig($key, array_flip($indexedCustom), false);
219: unset($config[$key]);
220:
221: return $config;
222: }
223:
224: /**
225: * verifyConfig
226: *
227: * Checks that implemented keys contain values pointing at callable.
228: *
229: * @return void
230: * @throws \Cake\Core\Exception\Exception if config are invalid
231: */
232: public function verifyConfig()
233: {
234: $keys = ['implementedFinders', 'implementedMethods'];
235: foreach ($keys as $key) {
236: if (!isset($this->_config[$key])) {
237: continue;
238: }
239:
240: foreach ($this->_config[$key] as $method) {
241: if (!is_callable([$this, $method])) {
242: throw new Exception(sprintf('The method %s is not callable on class %s', $method, get_class($this)));
243: }
244: }
245: }
246: }
247:
248: /**
249: * Gets the Model callbacks this behavior is interested in.
250: *
251: * By defining one of the callback methods a behavior is assumed
252: * to be interested in the related event.
253: *
254: * Override this method if you need to add non-conventional event listeners.
255: * Or if you want your behavior to listen to non-standard events.
256: *
257: * @return array
258: */
259: public function implementedEvents()
260: {
261: $eventMap = [
262: 'Model.beforeMarshal' => 'beforeMarshal',
263: 'Model.beforeFind' => 'beforeFind',
264: 'Model.beforeSave' => 'beforeSave',
265: 'Model.afterSave' => 'afterSave',
266: 'Model.afterSaveCommit' => 'afterSaveCommit',
267: 'Model.beforeDelete' => 'beforeDelete',
268: 'Model.afterDelete' => 'afterDelete',
269: 'Model.afterDeleteCommit' => 'afterDeleteCommit',
270: 'Model.buildValidator' => 'buildValidator',
271: 'Model.buildRules' => 'buildRules',
272: 'Model.beforeRules' => 'beforeRules',
273: 'Model.afterRules' => 'afterRules',
274: ];
275: $config = $this->getConfig();
276: $priority = isset($config['priority']) ? $config['priority'] : null;
277: $events = [];
278:
279: foreach ($eventMap as $event => $method) {
280: if (!method_exists($this, $method)) {
281: continue;
282: }
283: if ($priority === null) {
284: $events[$event] = $method;
285: } else {
286: $events[$event] = [
287: 'callable' => $method,
288: 'priority' => $priority
289: ];
290: }
291: }
292:
293: return $events;
294: }
295:
296: /**
297: * implementedFinders
298: *
299: * Provides an alias->methodname map of which finders a behavior implements. Example:
300: *
301: * ```
302: * [
303: * 'this' => 'findThis',
304: * 'alias' => 'findMethodName'
305: * ]
306: * ```
307: *
308: * With the above example, a call to `$Table->find('this')` will call `$Behavior->findThis()`
309: * and a call to `$Table->find('alias')` will call `$Behavior->findMethodName()`
310: *
311: * It is recommended, though not required, to define implementedFinders in the config property
312: * of child classes such that it is not necessary to use reflections to derive the available
313: * method list. See core behaviors for examples
314: *
315: * @return array
316: * @throws \ReflectionException
317: */
318: public function implementedFinders()
319: {
320: $methods = $this->getConfig('implementedFinders');
321: if (isset($methods)) {
322: return $methods;
323: }
324:
325: return $this->_reflectionCache()['finders'];
326: }
327:
328: /**
329: * implementedMethods
330: *
331: * Provides an alias->methodname map of which methods a behavior implements. Example:
332: *
333: * ```
334: * [
335: * 'method' => 'method',
336: * 'aliasedmethod' => 'somethingElse'
337: * ]
338: * ```
339: *
340: * With the above example, a call to `$Table->method()` will call `$Behavior->method()`
341: * and a call to `$Table->aliasedmethod()` will call `$Behavior->somethingElse()`
342: *
343: * It is recommended, though not required, to define implementedFinders in the config property
344: * of child classes such that it is not necessary to use reflections to derive the available
345: * method list. See core behaviors for examples
346: *
347: * @return array
348: * @throws \ReflectionException
349: */
350: public function implementedMethods()
351: {
352: $methods = $this->getConfig('implementedMethods');
353: if (isset($methods)) {
354: return $methods;
355: }
356:
357: return $this->_reflectionCache()['methods'];
358: }
359:
360: /**
361: * Gets the methods implemented by this behavior
362: *
363: * Uses the implementedEvents() method to exclude callback methods.
364: * Methods starting with `_` will be ignored, as will methods
365: * declared on Cake\ORM\Behavior
366: *
367: * @return array
368: * @throws \ReflectionException
369: */
370: protected function _reflectionCache()
371: {
372: $class = get_class($this);
373: if (isset(self::$_reflectionCache[$class])) {
374: return self::$_reflectionCache[$class];
375: }
376:
377: $events = $this->implementedEvents();
378: $eventMethods = [];
379: foreach ($events as $e => $binding) {
380: if (is_array($binding) && isset($binding['callable'])) {
381: /* @var string $callable */
382: $callable = $binding['callable'];
383: $binding = $callable;
384: }
385: $eventMethods[$binding] = true;
386: }
387:
388: $baseClass = 'Cake\ORM\Behavior';
389: if (isset(self::$_reflectionCache[$baseClass])) {
390: $baseMethods = self::$_reflectionCache[$baseClass];
391: } else {
392: $baseMethods = get_class_methods($baseClass);
393: self::$_reflectionCache[$baseClass] = $baseMethods;
394: }
395:
396: $return = [
397: 'finders' => [],
398: 'methods' => []
399: ];
400:
401: $reflection = new ReflectionClass($class);
402:
403: foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
404: $methodName = $method->getName();
405: if (in_array($methodName, $baseMethods, true) ||
406: isset($eventMethods[$methodName])
407: ) {
408: continue;
409: }
410:
411: if (substr($methodName, 0, 4) === 'find') {
412: $return['finders'][lcfirst(substr($methodName, 4))] = $methodName;
413: } else {
414: $return['methods'][$methodName] = $methodName;
415: }
416: }
417:
418: return self::$_reflectionCache[$class] = $return;
419: }
420: }
421: