pictcode / lib / Cake / Model / Behavior / ContainableBehavior.php @ 0b1b8047
履歴 | 表示 | アノテート | ダウンロード (14.132 KB)
1 | 635eef61 | spyder1211 | <?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 | } |