Subversion-Projekte lars-tiefland.cakephp

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/* SVN FILE: $Id: containable.php 7945 2008-12-19 02:16:01Z gwoo $ */
3
/**
4
 * Behavior for binding management.
5
 *
6
 * Behavior to simplify manipulating a model's bindings when doing a find operation
7
 *
8
 * PHP versions 4 and 5
9
 *
10
 * CakePHP(tm) :  Rapid Development Framework (http://www.cakephp.org)
11
 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
12
 *
13
 * Licensed under The MIT License
14
 * Redistributions of files must retain the above copyright notice.
15
 *
16
 * @filesource
17
 * @copyright     Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
18
 * @link          http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
19
 * @package       cake
20
 * @subpackage    cake.cake.console.libs
21
 * @since         CakePHP(tm) v 1.2.0.5669
22
 * @version       $Revision: 7945 $
23
 * @modifiedby    $LastChangedBy: gwoo $
24
 * @lastmodified  $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $
25
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
26
 */
27
/**
28
 * Behavior to allow for dynamic and atomic manipulation of a Model's associations used for a find call. Most useful for limiting
29
 * the amount of associations and data returned.
30
 *
31
 * @package       cake
32
 * @subpackage    cake.cake.console.libs
33
 */
34
class ContainableBehavior extends ModelBehavior {
35
/**
36
 * Types of relationships available for models
37
 *
38
 * @var array
39
 * @access private
40
 */
41
	var $types = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
42
/**
43
 * Runtime configuration for this behavior
44
 *
45
 * @var array
46
 * @access private
47
 */
48
	var $runtime = array();
49
/**
50
 * Initiate behavior for the model using specified settings. Available settings:
51
 *
52
 * - recursive: (boolean, optional) set to true to allow containable to automatically
53
 * 				determine the recursiveness level needed to fetch specified models,
54
 * 				and set the model recursiveness to this level. setting it to false
55
 * 				disables this feature. DEFAULTS TO: true
56
 *
57
 * - notices:	(boolean, optional) issues E_NOTICES for bindings referenced in a
58
 * 				containable call that are not valid. DEFAULTS TO: true
59
 *
60
 * - autoFields: (boolean, optional) auto-add needed fields to fetch requested
61
 * 				bindings. DEFAULTS TO: true
62
 *
63
 * @param object $Model Model using the behavior
64
 * @param array $settings Settings to override for model.
65
 * @access public
66
 */
67
	function setup(&$Model, $settings = array()) {
68
		if (!isset($this->settings[$Model->alias])) {
69
			$this->settings[$Model->alias] = array('recursive' => true, 'notices' => true, 'autoFields' => true);
70
		}
71
		if (!is_array($settings)) {
72
			$settings = array();
73
		}
74
		$this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings);
75
	}
76
/**
77
 * Runs before a find() operation. Used to allow 'contain' setting
78
 * as part of the find call, like this:
79
 *
80
 * Model->find('all', array('contain' => array('Model1', 'Model2')));
81
 *
82
 * Model->find('all', array('contain' => array(
83
 * 	'Model1' => array('Model11', 'Model12'),
84
 * 	'Model2',
85
 * 	'Model3' => array(
86
 * 		'Model31' => 'Model311',
87
 * 		'Model32',
88
 * 		'Model33' => array('Model331', 'Model332')
89
 * )));
90
 *
91
 * @param object $Model	Model using the behavior
92
 * @param array $query Query parameters as set by cake
93
 * @return array
94
 * @access public
95
 */
96
	function beforeFind(&$Model, $query) {
97
		$reset = (isset($query['reset']) ? $query['reset'] : true);
98
		$noContain = ((isset($this->runtime[$Model->alias]['contain']) && empty($this->runtime[$Model->alias]['contain'])) || (isset($query['contain']) && empty($query['contain'])));
99
		$contain = array();
100
		if (isset($this->runtime[$Model->alias]['contain'])) {
101
			$contain = $this->runtime[$Model->alias]['contain'];
102
			unset($this->runtime[$Model->alias]['contain']);
103
		}
104
		if (isset($query['contain'])) {
105
			$contain = array_merge($contain, (array)$query['contain']);
106
		}
107
		if ($noContain || !$contain || in_array($contain, array(null, false), true) || (isset($contain[0]) && $contain[0] === null)) {
108
			if ($noContain) {
109
				$query['recursive'] = -1;
110
			}
111
			return $query;
112
		}
113
		if ((isset($contain[0]) && is_bool($contain[0])) || is_bool(end($contain))) {
114
			$reset = is_bool(end($contain))
115
				? array_pop($contain)
116
				: array_shift($contain);
117
		}
118
		$containments = $this->containments($Model, $contain);
119
		$map = $this->containmentsMap($containments);
120
 
121
		$mandatory = array();
122
		foreach ($containments['models'] as $name => $model) {
123
			$instance =& $model['instance'];
124
			$needed = $this->fieldDependencies($instance, $map, false);
125
			if (!empty($needed)) {
126
				$mandatory = array_merge($mandatory, $needed);
127
			}
128
			if ($contain) {
129
				$backupBindings = array();
130
				foreach ($this->types as $relation) {
131
					$backupBindings[$relation] = $instance->{$relation};
132
				}
133
				foreach ($this->types as $type) {
134
					$unbind = array();
135
					foreach ($instance->{$type} as $assoc => $options) {
136
						if (!isset($model['keep'][$assoc])) {
137
							$unbind[] = $assoc;
138
						}
139
					}
140
					if (!empty($unbind)) {
141
						if (!$reset && empty($instance->__backOriginalAssociation)) {
142
							$instance->__backOriginalAssociation = $backupBindings;
143
						} else if ($reset && empty($instance->__backContainableAssociation)) {
144
							$instance->__backContainableAssociation = $backupBindings;
145
						}
146
						$instance->unbindModel(array($type => $unbind), $reset);
147
					}
148
					foreach ($instance->{$type} as $assoc => $options) {
149
						if (isset($model['keep'][$assoc]) && !empty($model['keep'][$assoc])) {
150
							if (isset($model['keep'][$assoc]['fields'])) {
151
								$model['keep'][$assoc]['fields'] = $this->fieldDependencies($containments['models'][$assoc]['instance'], $map, $model['keep'][$assoc]['fields']);
152
							}
153
							if (!$reset && empty($instance->__backOriginalAssociation)) {
154
								$instance->__backOriginalAssociation = $backupBindings;
155
							} else if ($reset) {
156
								$instance->__backAssociation[$type] = $instance->{$type};
157
							}
158
							$instance->{$type}[$assoc] = array_merge($instance->{$type}[$assoc], $model['keep'][$assoc]);
159
						}
160
						if (!$reset) {
161
							$instance->__backInnerAssociation[] = $assoc;
162
						}
163
					}
164
				}
165
			}
166
		}
167
 
168
		if ($this->settings[$Model->alias]['recursive']) {
169
			$query['recursive'] = (isset($query['recursive'])) ? $query['recursive'] : $containments['depth'];
170
		}
171
 
172
		$autoFields = ($this->settings[$Model->alias]['autoFields']
173
					&& !in_array($Model->findQueryType, array('list', 'count'))
174
					&& !empty($query['fields']));
175
		if (!$autoFields) {
176
			return $query;
177
		}
178
 
179
		$query['fields'] = (array)$query['fields'];
180
		if (!empty($Model->belongsTo)) {
181
			foreach ($Model->belongsTo as $assoc => $data) {
182
				if (!empty($data['fields'])) {
183
					foreach ((array) $data['fields'] as $field) {
184
						$query['fields'][] = (strpos($field, '.') === false ? $assoc . '.' : '') . $field;
185
					}
186
				}
187
			}
188
		}
189
		if (!empty($mandatory[$Model->alias])) {
190
			foreach ($mandatory[$Model->alias] as $field) {
191
				if ($field == '--primaryKey--') {
192
					$field = $Model->primaryKey;
193
				} else if (preg_match('/^.+\.\-\-[^-]+\-\-$/', $field)) {
194
					list($modelName, $field) = explode('.', $field);
195
					$field = $modelName . '.' . (($field === '--primaryKey--') ? $Model->$modelName->primaryKey : $field);
196
				}
197
				$query['fields'][] = $field;
198
			}
199
		}
200
		$query['fields'] = array_unique($query['fields']);
201
		return $query;
202
	}
203
/**
204
 * Resets original associations on models that may have receive multiple,
205
 * subsequent unbindings.
206
 *
207
 * @param object $Model Model on which we are resetting
208
 * @param array $results Results of the find operation
209
 * @param bool $primary true if this is the primary model that issued the find operation, false otherwise
210
 * @access public
211
 */
212
	function afterFind(&$Model, $results, $primary) {
213
		if (!empty($Model->__backContainableAssociation)) {
214
			foreach ($Model->__backContainableAssociation as $relation => $bindings) {
215
				$Model->{$relation} = $bindings;
216
				unset($Model->__backContainableAssociation);
217
			}
218
		}
219
	}
220
/**
221
 * Unbinds all relations from a model except the specified ones. Calling this function without
222
 * parameters unbinds all related models.
223
 *
224
 * @param object $Model Model on which binding restriction is being applied
225
 * @return void
226
 * @access public
227
 */
228
	function contain(&$Model) {
229
		$args = func_get_args();
230
		$contain = call_user_func_array('am', array_slice($args, 1));
231
		$this->runtime[$Model->alias]['contain'] = $contain;
232
	}
233
/**
234
 * Permanently restore the original binding settings of given model, useful
235
 * for restoring the bindings after using 'reset' => false as part of the
236
 * contain call.
237
 *
238
 * @param object $Model Model on which to reset bindings
239
 * @return void
240
 * @access public
241
 */
242
	function resetBindings(&$Model) {
243
		if (!empty($Model->__backOriginalAssociation)) {
244
			$Model->__backAssociation = $Model->__backOriginalAssociation;
245
			unset($Model->__backOriginalAssociation);
246
		}
247
		$Model->resetAssociations();
248
		if (!empty($Model->__backInnerAssociation)) {
249
			$assocs = $Model->__backInnerAssociation;
250
			unset($Model->__backInnerAssociation);
251
			foreach ($assocs as $currentModel) {
252
				$this->resetBindings($Model->$currentModel);
253
			}
254
		}
255
	}
256
/**
257
 * Process containments for model.
258
 *
259
 * @param object $Model Model on which binding restriction is being applied
260
 * @param array $contain Parameters to use for restricting this model
261
 * @param array $containments Current set of containments
262
 * @param bool $throwErrors Wether unexisting bindings show throw errors
263
 * @return array Containments
264
 * @access public
265
 */
266
	function containments(&$Model, $contain, $containments = array(), $throwErrors = null) {
267
		$options = array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery');
268
		$keep = array();
269
		$depth = array();
270
		if ($throwErrors === null) {
271
			$throwErrors = (empty($this->settings[$Model->alias]) ? true : $this->settings[$Model->alias]['notices']);
272
		}
273
		foreach ((array)$contain as $name => $children) {
274
			if (is_numeric($name)) {
275
				$name = $children;
276
				$children = array();
277
			}
278
			if (preg_match('/(?<!\.)\(/', $name)) {
279
				$name = str_replace('(', '.(', $name);
280
			}
281
			if (strpos($name, '.') !== false) {
282
				$chain = explode('.', $name);
283
				$name = array_shift($chain);
284
				$children = array(join('.', $chain) => $children);
285
			}
286
 
287
			$children = (array)$children;
288
			foreach ($children as $key => $val) {
289
				if (is_string($key) && is_string($val) && !in_array($key, $options, true)) {
290
					$children[$key] = (array) $val;
291
				}
292
			}
293
 
294
			$keys = array_keys($children);
295
			if ($keys && isset($children[0])) {
296
				$keys = array_merge(array_values($children), $keys);
297
			}
298
 
299
			foreach ($keys as $i => $key) {
300
				if (is_array($key)) {
301
					continue;
302
				}
303
				$optionKey = in_array($key, $options, true);
304
				if (!$optionKey && is_string($key) && preg_match('/^[a-z(]/', $key) && (!isset($Model->{$key}) || !is_object($Model->{$key}))) {
305
					$option = 'fields';
306
					$val = array($key);
307
					if ($key{0} == '(') {
308
						$val = preg_split('/\s*,\s*/', substr(substr($key, 1), 0, -1));
309
					} elseif (preg_match('/ASC|DESC$/', $key)) {
310
						$option = 'order';
311
						$val = $Model->{$name}->alias.'.'.$key;
312
					} elseif (preg_match('/[ =!]/', $key)) {
313
						$option = 'conditions';
314
						$val = $Model->{$name}->alias.'.'.$key;
315
					}
316
					$children[$option] = isset($children[$option]) ? array_merge((array) $children[$option], (array) $val) : $val;
317
					$newChildren = null;
318
					if (!empty($name) && !empty($children[$key])) {
319
						$newChildren = $children[$key];
320
					}
321
					unset($children[$key], $children[$i]);
322
					$key = $option;
323
					$optionKey = true;
324
					if (!empty($newChildren)) {
325
						$children = Set::merge($children, $newChildren);
326
					}
327
				}
328
				if ($optionKey && isset($children[$key])) {
329
					if (!empty($keep[$name][$key]) && is_array($keep[$name][$key])) {
330
						$keep[$name][$key] = array_merge((isset($keep[$name][$key]) ? $keep[$name][$key] : array()), (array) $children[$key]);
331
					} else {
332
						$keep[$name][$key] = $children[$key];
333
					}
334
					unset($children[$key]);
335
				}
336
			}
337
 
338
			if (!isset($Model->{$name}) || !is_object($Model->{$name})) {
339
				if ($throwErrors) {
340
					trigger_error(sprintf(__('Model "%s" is not associated with model "%s"', true), $Model->alias, $name), E_USER_WARNING);
341
				}
342
				continue;
343
			}
344
 
345
			$containments = $this->containments($Model->{$name}, $children, $containments);
346
			$depths[] = $containments['depth'] + 1;
347
			if (!isset($keep[$name])) {
348
				$keep[$name] = array();
349
			}
350
		}
351
 
352
		if (!isset($containments['models'][$Model->alias])) {
353
			$containments['models'][$Model->alias] = array('keep' => array(),'instance' => &$Model);
354
		}
355
 
356
		$containments['models'][$Model->alias]['keep'] = array_merge($containments['models'][$Model->alias]['keep'], $keep);
357
		$containments['depth'] = empty($depths) ? 0 : max($depths);
358
		return $containments;
359
	}
360
/**
361
 * Calculate needed fields to fetch the required bindings for the given model.
362
 *
363
 * @param object $Model Model
364
 * @param array $map Map of relations for given model
365
 * @param mixed $fields If array, fields to initially load, if false use $Model as primary model
366
 * @return array Fields
367
 * @access public
368
 */
369
	function fieldDependencies(&$Model, $map, $fields = array()) {
370
		if ($fields === false) {
371
			foreach ($map as $parent => $children) {
372
				foreach ($children as $type => $bindings) {
373
					foreach ($bindings as $dependency) {
374
						if ($type == 'hasAndBelongsToMany') {
375
							$fields[$parent][] = '--primaryKey--';
376
						} else if ($type == 'belongsTo') {
377
							$fields[$parent][] = $dependency . '.--primaryKey--';
378
						}
379
					}
380
				}
381
			}
382
			return $fields;
383
		}
384
		if (empty($map[$Model->alias])) {
385
			return $fields;
386
		}
387
		foreach ($map[$Model->alias] as $type => $bindings) {
388
			foreach ($bindings as $dependency) {
389
				$innerFields = array();
390
				switch ($type) {
391
					case 'belongsTo':
392
						$fields[] = $Model->{$type}[$dependency]['foreignKey'];
393
						break;
394
					case 'hasOne':
395
					case 'hasMany':
396
						$innerFields[] = $Model->$dependency->primaryKey;
397
						$fields[] = $Model->primaryKey;
398
						break;
399
				}
400
				if (!empty($innerFields) && !empty($Model->{$type}[$dependency]['fields'])) {
401
					$Model->{$type}[$dependency]['fields'] = array_unique(array_merge($Model->{$type}[$dependency]['fields'], $innerFields));
402
				}
403
			}
404
		}
405
		return array_unique($fields);
406
	}
407
/**
408
 * Build the map of containments
409
 *
410
 * @param array $containments Containments
411
 * @return array Built containments
412
 * @access public
413
 */
414
	function containmentsMap($containments) {
415
		$map = array();
416
		foreach ($containments['models'] as $name => $model) {
417
			$instance =& $model['instance'];
418
			foreach ($this->types as $type) {
419
				foreach ($instance->{$type} as $assoc => $options) {
420
					if (isset($model['keep'][$assoc])) {
421
						$map[$name][$type] = isset($map[$name][$type]) ? array_merge($map[$name][$type], (array)$assoc) : (array)$assoc;
422
					}
423
				}
424
			}
425
		}
426
		return $map;
427
	}
428
}
429
?>