cake/libs/model/model_behavior.php

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-2011, 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-2011, 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  
535