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