1: <?php
2: /**
3: * Model behaviors base class.
4: *
5: * Adds methods and automagic functionality to Cake Models.
6: *
7: * PHP versions 4 and 5
8: *
9: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10: * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
11: *
12: * Licensed under The MIT License
13: * Redistributions of files must retain the above copyright notice.
14: *
15: * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
16: * @link http://cakephp.org CakePHP(tm) Project
17: * @package cake
18: * @subpackage cake.cake.libs.model
19: * @since CakePHP(tm) v 1.2.0.0
20: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
21: */
22:
23: /**
24: * Model behavior base class.
25: *
26: * Defines the Behavior interface, and contains common model interaction functionality.
27: *
28: * @package cake
29: * @subpackage cake.cake.libs.model
30: */
31: class ModelBehavior extends Object {
32:
33: /**
34: * Contains configuration settings for use with individual model objects. This
35: * is used because if multiple models use this Behavior, each will use the same
36: * object instance. Individual model settings should be stored as an
37: * associative array, keyed off of the model name.
38: *
39: * @var array
40: * @access public
41: * @see Model::$alias
42: */
43: var $settings = array();
44:
45: /**
46: * Allows the mapping of preg-compatible regular expressions to public or
47: * private methods in this class, where the array key is a /-delimited regular
48: * expression, and the value is a class method. Similar to the functionality of
49: * the findBy* / findAllBy* magic methods.
50: *
51: * @var array
52: * @access public
53: */
54: var $mapMethods = array();
55:
56: /**
57: * Setup this behavior with the specified configuration settings.
58: *
59: * @param object $model Model using this behavior
60: * @param array $config Configuration settings for $model
61: * @access public
62: */
63: function setup(&$model, $config = array()) { }
64:
65: /**
66: * Clean up any initialization this behavior has done on a model. Called when a behavior is dynamically
67: * detached from a model using Model::detach().
68: *
69: * @param object $model Model using this behavior
70: * @access public
71: * @see BehaviorCollection::detach()
72: */
73: function cleanup(&$model) {
74: if (isset($this->settings[$model->alias])) {
75: unset($this->settings[$model->alias]);
76: }
77: }
78:
79: /**
80: * Before find callback
81: *
82: * @param object $model Model using this behavior
83: * @param array $queryData Data used to execute this query, i.e. conditions, order, etc.
84: * @return mixed False if the operation should abort. An array will replace the value of $query.
85: * @access public
86: */
87: function beforeFind(&$model, $query) { }
88:
89: /**
90: * After find callback. Can be used to modify any results returned by find and findAll.
91: *
92: * @param object $model Model using this behavior
93: * @param mixed $results The results of the find operation
94: * @param boolean $primary Whether this model is being queried directly (vs. being queried as an association)
95: * @return mixed An array value will replace the value of $results - any other value will be ignored.
96: * @access public
97: */
98: function afterFind(&$model, $results, $primary) { }
99:
100: /**
101: * Before validate callback
102: *
103: * @param object $model Model using this behavior
104: * @return mixed False if the operation should abort. Any other result will continue.
105: * @access public
106: */
107: function beforeValidate(&$model) { }
108:
109: /**
110: * Before save callback
111: *
112: * @param object $model Model using this behavior
113: * @return mixed False if the operation should abort. Any other result will continue.
114: * @access public
115: */
116: function beforeSave(&$model) { }
117:
118: /**
119: * After save callback
120: *
121: * @param object $model Model using this behavior
122: * @param boolean $created True if this save created a new record
123: * @access public
124: */
125: function afterSave(&$model, $created) { }
126:
127: /**
128: * Before delete callback
129: *
130: * @param object $model Model using this behavior
131: * @param boolean $cascade If true records that depend on this record will also be deleted
132: * @return mixed False if the operation should abort. Any other result will continue.
133: * @access public
134: */
135: function beforeDelete(&$model, $cascade = true) { }
136:
137: /**
138: * After delete callback
139: *
140: * @param object $model Model using this behavior
141: * @access public
142: */
143: function afterDelete(&$model) { }
144:
145: /**
146: * DataSource error callback
147: *
148: * @param object $model Model using this behavior
149: * @param string $error Error generated in DataSource
150: * @access public
151: */
152: function onError(&$model, $error) { }
153:
154: /**
155: * Overrides Object::dispatchMethod to account for PHP4's broken reference support
156: *
157: * @see Object::dispatchMethod
158: * @access public
159: * @return mixed
160: */
161: function dispatchMethod(&$model, $method, $params = array()) {
162: if (empty($params)) {
163: return $this->{$method}($model);
164: }
165: $params = array_values($params);
166:
167: switch (count($params)) {
168: case 1:
169: return $this->{$method}($model, $params[0]);
170: case 2:
171: return $this->{$method}($model, $params[0], $params[1]);
172: case 3:
173: return $this->{$method}($model, $params[0], $params[1], $params[2]);
174: case 4:
175: return $this->{$method}($model, $params[0], $params[1], $params[2], $params[3]);
176: case 5:
177: return $this->{$method}($model, $params[0], $params[1], $params[2], $params[3], $params[4]);
178: default:
179: $params = array_merge(array(&$model), $params);
180: return call_user_func_array(array(&$this, $method), $params);
181: break;
182: }
183: }
184:
185: /**
186: * If $model's whitelist property is non-empty, $field will be added to it.
187: * Note: this method should *only* be used in beforeValidate or beforeSave to ensure
188: * that it only modifies the whitelist for the current save operation. Also make sure
189: * you explicitly set the value of the field which you are allowing.
190: *
191: * @param object $model Model using this behavior
192: * @param string $field Field to be added to $model's whitelist
193: * @access protected
194: * @return void
195: */
196: function _addToWhitelist(&$model, $field) {
197: if (is_array($field)) {
198: foreach ($field as $f) {
199: $this->_addToWhitelist($model, $f);
200: }
201: return;
202: }
203: if (!empty($model->whitelist) && !in_array($field, $model->whitelist)) {
204: $model->whitelist[] = $field;
205: }
206: }
207: }
208:
209: /**
210: * Model behavior collection class.
211: *
212: * Defines the Behavior interface, and contains common model interaction functionality.
213: *
214: * @package cake
215: * @subpackage cake.cake.libs.model
216: */
217: class BehaviorCollection extends Object {
218:
219: /**
220: * Stores a reference to the attached name
221: *
222: * @var string
223: * @access public
224: */
225: var $modelName = null;
226:
227: /**
228: * Lists the currently-attached behavior objects
229: *
230: * @var array
231: * @access private
232: */
233: var $_attached = array();
234:
235: /**
236: * Lists the currently-attached behavior objects which are disabled
237: *
238: * @var array
239: * @access private
240: */
241: var $_disabled = array();
242:
243: /**
244: * Keeps a list of all methods of attached behaviors
245: *
246: * @var array
247: */
248: var $__methods = array();
249:
250: /**
251: * Keeps a list of all methods which have been mapped with regular expressions
252: *
253: * @var array
254: */
255: var $__mappedMethods = array();
256:
257: /**
258: * Attaches a model object and loads a list of behaviors
259: *
260: * @access public
261: * @return void
262: */
263: function init($modelName, $behaviors = array()) {
264: $this->modelName = $modelName;
265:
266: if (!empty($behaviors)) {
267: foreach (Set::normalize($behaviors) as $behavior => $config) {
268: $this->attach($behavior, $config);
269: }
270: }
271: }
272:
273: /**
274: * Attaches a behavior to a model
275: *
276: * @param string $behavior CamelCased name of the behavior to load
277: * @param array $config Behavior configuration parameters
278: * @return boolean True on success, false on failure
279: * @access public
280: */
281: function attach($behavior, $config = array()) {
282: list($plugin, $name) = pluginSplit($behavior);
283: $class = $name . 'Behavior';
284:
285: if (!App::import('Behavior', $behavior)) {
286: $this->cakeError('missingBehaviorFile', array(array(
287: 'behavior' => $behavior,
288: 'file' => Inflector::underscore($behavior) . '.php',
289: 'code' => 500,
290: 'base' => '/'
291: )));
292: return false;
293: }
294: if (!class_exists($class)) {
295: $this->cakeError('missingBehaviorClass', array(array(
296: 'behavior' => $class,
297: 'file' => Inflector::underscore($class) . '.php',
298: 'code' => 500,
299: 'base' => '/'
300: )));
301: return false;
302: }
303:
304: if (!isset($this->{$name})) {
305: if (ClassRegistry::isKeySet($class)) {
306: if (PHP5) {
307: $this->{$name} = ClassRegistry::getObject($class);
308: } else {
309: $this->{$name} =& ClassRegistry::getObject($class);
310: }
311: } else {
312: if (PHP5) {
313: $this->{$name} = new $class;
314: } else {
315: $this->{$name} =& new $class;
316: }
317: ClassRegistry::addObject($class, $this->{$name});
318: if (!empty($plugin)) {
319: ClassRegistry::addObject($plugin.'.'.$class, $this->{$name});
320: }
321: }
322: } elseif (isset($this->{$name}->settings) && isset($this->{$name}->settings[$this->modelName])) {
323: if ($config !== null && $config !== false) {
324: $config = array_merge($this->{$name}->settings[$this->modelName], $config);
325: } else {
326: $config = array();
327: }
328: }
329: if (empty($config)) {
330: $config = array();
331: }
332: $this->{$name}->setup(ClassRegistry::getObject($this->modelName), $config);
333:
334: foreach ($this->{$name}->mapMethods as $method => $alias) {
335: $this->__mappedMethods[$method] = array($alias, $name);
336: }
337: $methods = get_class_methods($this->{$name});
338: $parentMethods = array_flip(get_class_methods('ModelBehavior'));
339: $callbacks = array(
340: 'setup', 'cleanup', 'beforeFind', 'afterFind', 'beforeSave', 'afterSave',
341: 'beforeDelete', 'afterDelete', 'afterError'
342: );
343:
344: foreach ($methods as $m) {
345: if (!isset($parentMethods[$m])) {
346: $methodAllowed = (
347: $m[0] != '_' && !array_key_exists($m, $this->__methods) &&
348: !in_array($m, $callbacks)
349: );
350: if ($methodAllowed) {
351: $this->__methods[$m] = array($m, $name);
352: }
353: }
354: }
355:
356: if (!in_array($name, $this->_attached)) {
357: $this->_attached[] = $name;
358: }
359: if (in_array($name, $this->_disabled) && !(isset($config['enabled']) && $config['enabled'] === false)) {
360: $this->enable($name);
361: } elseif (isset($config['enabled']) && $config['enabled'] === false) {
362: $this->disable($name);
363: }
364: return true;
365: }
366:
367: /**
368: * Detaches a behavior from a model
369: *
370: * @param string $name CamelCased name of the behavior to unload
371: * @return void
372: * @access public
373: */
374: function detach($name) {
375: list($plugin, $name) = pluginSplit($name);
376: if (isset($this->{$name})) {
377: $this->{$name}->cleanup(ClassRegistry::getObject($this->modelName));
378: unset($this->{$name});
379: }
380: foreach ($this->__methods as $m => $callback) {
381: if (is_array($callback) && $callback[1] == $name) {
382: unset($this->__methods[$m]);
383: }
384: }
385: $this->_attached = array_values(array_diff($this->_attached, (array)$name));
386: }
387:
388: /**
389: * Enables callbacks on a behavior or array of behaviors
390: *
391: * @param mixed $name CamelCased name of the behavior(s) to enable (string or array)
392: * @return void
393: * @access public
394: */
395: function enable($name) {
396: $this->_disabled = array_diff($this->_disabled, (array)$name);
397: }
398:
399: /**
400: * Disables callbacks on a behavior or array of behaviors. Public behavior methods are still
401: * callable as normal.
402: *
403: * @param mixed $name CamelCased name of the behavior(s) to disable (string or array)
404: * @return void
405: * @access public
406: */
407: function disable($name) {
408: foreach ((array)$name as $behavior) {
409: if (in_array($behavior, $this->_attached) && !in_array($behavior, $this->_disabled)) {
410: $this->_disabled[] = $behavior;
411: }
412: }
413: }
414:
415: /**
416: * Gets the list of currently-enabled behaviors, or, the current status of a single behavior
417: *
418: * @param string $name Optional. The name of the behavior to check the status of. If omitted,
419: * returns an array of currently-enabled behaviors
420: * @return mixed If $name is specified, returns the boolean status of the corresponding behavior.
421: * Otherwise, returns an array of all enabled behaviors.
422: * @access public
423: */
424: function enabled($name = null) {
425: if (!empty($name)) {
426: return (in_array($name, $this->_attached) && !in_array($name, $this->_disabled));
427: }
428: return array_diff($this->_attached, $this->_disabled);
429: }
430:
431: /**
432: * Dispatches a behavior method
433: *
434: * @return array All methods for all behaviors attached to this object
435: * @access public
436: */
437: function dispatchMethod(&$model, $method, $params = array(), $strict = false) {
438: $methods = array_keys($this->__methods);
439: foreach ($methods as $key => $value) {
440: $methods[$key] = strtolower($value);
441: }
442: $method = strtolower($method);
443: $check = array_flip($methods);
444: $found = isset($check[$method]);
445: $call = null;
446:
447: if ($strict && !$found) {
448: trigger_error(sprintf(__("BehaviorCollection::dispatchMethod() - Method %s not found in any attached behavior", true), $method), E_USER_WARNING);
449: return null;
450: } elseif ($found) {
451: $methods = array_combine($methods, array_values($this->__methods));
452: $call = $methods[$method];
453: } else {
454: $count = count($this->__mappedMethods);
455: $mapped = array_keys($this->__mappedMethods);
456:
457: for ($i = 0; $i < $count; $i++) {
458: if (preg_match($mapped[$i] . 'i', $method)) {
459: $call = $this->__mappedMethods[$mapped[$i]];
460: array_unshift($params, $method);
461: break;
462: }
463: }
464: }
465:
466: if (!empty($call)) {
467: return $this->{$call[1]}->dispatchMethod($model, $call[0], $params);
468: }
469: return array('unhandled');
470: }
471:
472: /**
473: * Dispatches a behavior callback on all attached behavior objects
474: *
475: * @param model $model
476: * @param string $callback
477: * @param array $params
478: * @param array $options
479: * @return mixed
480: * @access public
481: */
482: function trigger(&$model, $callback, $params = array(), $options = array()) {
483: if (empty($this->_attached)) {
484: return true;
485: }
486: $options = array_merge(array('break' => false, 'breakOn' => array(null, false), 'modParams' => false), $options);
487: $count = count($this->_attached);
488:
489: for ($i = 0; $i < $count; $i++) {
490: $name = $this->_attached[$i];
491: if (in_array($name, $this->_disabled)) {
492: continue;
493: }
494: $result = $this->{$name}->dispatchMethod($model, $callback, $params);
495:
496: if ($options['break'] && ($result === $options['breakOn'] || (is_array($options['breakOn']) && in_array($result, $options['breakOn'], true)))) {
497: return $result;
498: } elseif ($options['modParams'] && is_array($result)) {
499: $params[0] = $result;
500: }
501: }
502: if ($options['modParams'] && isset($params[0])) {
503: return $params[0];
504: }
505: return true;
506: }
507:
508: /**
509: * Gets the method list for attached behaviors, i.e. all public, non-callback methods
510: *
511: * @return array All public methods for all behaviors attached to this collection
512: * @access public
513: */
514: function methods() {
515: return $this->__methods;
516: }
517:
518: /**
519: * Gets the list of attached behaviors, or, whether the given behavior is attached
520: *
521: * @param string $name Optional. The name of the behavior to check the status of. If omitted,
522: * returns an array of currently-attached behaviors
523: * @return mixed If $name is specified, returns the boolean status of the corresponding behavior.
524: * Otherwise, returns an array of all attached behaviors.
525: * @access public
526: */
527: function attached($name = null) {
528: if (!empty($name)) {
529: return (in_array($name, $this->_attached));
530: }
531: return $this->_attached;
532: }
533: }
534: