1
|
|
|
<?php |
|
|
|
|
2
|
|
|
App::uses('CakeText', 'Utility'); |
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* EagerLoader class |
6
|
|
|
* |
7
|
|
|
* @internal |
8
|
|
|
*/ |
9
|
|
|
class EagerLoader { |
|
|
|
|
10
|
|
|
|
11
|
|
|
private static $handlers = array(); // @codingStandardsIgnoreLine |
12
|
|
|
|
13
|
|
|
private $id; // @codingStandardsIgnoreLine |
14
|
|
|
|
15
|
|
|
private $metas = array(); // @codingStandardsIgnoreLine |
16
|
|
|
|
17
|
|
|
private $containOptions = array( // @codingStandardsIgnoreLine |
18
|
|
|
'conditions' => 1, |
19
|
|
|
'fields' => 1, |
20
|
|
|
'order' => 1, |
21
|
|
|
'limit' => 1, |
22
|
|
|
'offset' => 1, |
23
|
|
|
); |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Constructor |
27
|
|
|
*/ |
28
|
|
|
public function __construct() { |
29
|
|
|
ClassRegistry::init('EagerLoader.EagerLoaderModel'); |
30
|
|
|
|
31
|
|
|
if (class_exists('CakeText')) { |
32
|
|
|
$this->id = CakeText::uuid(); |
33
|
|
|
} else { |
34
|
|
|
App::uses('String', 'Utility'); |
35
|
|
|
$this->id = String::uuid(); |
36
|
|
|
} |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Handles beforeFind event |
41
|
|
|
* |
42
|
|
|
* @param Model $model Model |
43
|
|
|
* @param array $query Query |
44
|
|
|
* @return array Modified query |
45
|
|
|
*/ |
46
|
|
|
public static function handleBeforeFind(Model $model, $query) { |
47
|
|
|
if (is_array($query)) { |
48
|
|
|
if (isset($query['contain'])) { |
49
|
|
|
if ($query['contain'] === false) { |
50
|
|
|
$query['recursive'] = -1; |
51
|
|
|
} else { |
52
|
|
|
$EagerLoader = new EagerLoader(); |
53
|
|
|
$query = $EagerLoader->transformQuery($model, $query); |
54
|
|
|
|
55
|
|
|
self::$handlers[$EagerLoader->id] = $EagerLoader; |
56
|
|
|
if (count(self::$handlers) > 1000) { |
57
|
|
|
$id = key(self::$handlers); |
58
|
|
|
unset(self::$handlers[$id]); |
59
|
|
|
} |
60
|
|
|
} |
61
|
|
|
} |
62
|
|
|
} |
63
|
|
|
return $query; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Handles afterFind event |
68
|
|
|
* |
69
|
|
|
* @param Model $model Model |
70
|
|
|
* @param array $results Results |
71
|
|
|
* @return array Modified results |
72
|
|
|
* @throws UnexpectedValueException |
73
|
|
|
*/ |
74
|
|
|
public static function handleAfterFind(Model $model, $results) { |
75
|
|
|
if (is_array($results)) { |
76
|
|
|
$id = Hash::get($results, '0.EagerLoaderModel.id'); |
77
|
|
|
if ($id) { |
78
|
|
|
if (empty(self::$handlers[$id])) { |
79
|
|
|
throw new UnexpectedValueException(sprintf('EagerLoader "%s" is not found', $id)); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
$EagerLoader = self::$handlers[$id]; |
83
|
|
|
unset(self::$handlers[$id]); |
84
|
|
|
|
85
|
|
|
$results = $EagerLoader->transformResults($model, $results); |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
return $results; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Modifies the passed query to fetch the top level attachable associations. |
93
|
|
|
* |
94
|
|
|
* @param Model $model Model |
95
|
|
|
* @param array $query Query |
96
|
|
|
* @return array Modified query |
97
|
|
|
*/ |
98
|
|
|
private function transformQuery(Model $model, array $query) { // @codingStandardsIgnoreLine |
99
|
|
|
ClassRegistry::init('EagerLoader.EagerLoaderModel'); |
100
|
|
|
|
101
|
|
|
$contain = $this->reformatContain($query['contain']); |
102
|
|
|
foreach ($contain['contain'] as $key => $val) { |
103
|
|
|
$this->parseContain($model, $key, $val); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
$query = $this->attachAssociations($model, $model->alias, $query); |
107
|
|
|
|
108
|
|
|
$db = $model->getDataSource(); |
109
|
|
|
$value = $db->value($this->id); |
110
|
|
|
$name = $db->name('EagerLoaderModel' . '__' . 'id'); |
111
|
|
|
$query['fields'][] = "($value) AS $name"; |
112
|
|
|
$query['callbacks'] = true; |
113
|
|
|
|
114
|
|
|
return $query; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Modifies the results |
119
|
|
|
* |
120
|
|
|
* @param Model $model Model |
121
|
|
|
* @param array $results Results |
122
|
|
|
* @return array Modified results |
123
|
|
|
*/ |
124
|
|
|
private function transformResults(Model $model, array $results) { // @codingStandardsIgnoreLine |
|
|
|
|
125
|
|
|
foreach ($results as &$result) { |
126
|
|
|
unset($result['EagerLoaderModel']); |
127
|
|
|
} |
128
|
|
|
return $this->loadExternal($model, $model->alias, $results); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Modifies the query to fetch attachable associations. |
133
|
|
|
* |
134
|
|
|
* @param Model $model Model |
135
|
|
|
* @param string $path The target path of the model, such as 'User.Article' |
136
|
|
|
* @param array $query Query |
137
|
|
|
* @return array Modified query |
138
|
|
|
*/ |
139
|
|
|
private function attachAssociations(Model $model, $path, array $query) { // @codingStandardsIgnoreLine |
140
|
|
|
$query = $this->normalizeQuery($model, $query); |
141
|
|
|
|
142
|
|
|
foreach ($this->metas($path) as $meta) { |
143
|
|
|
extract($meta); |
144
|
|
|
if ($external) { |
145
|
|
|
$query = $this->addField($query, "$parentAlias.$parentKey"); |
146
|
|
|
} else { |
147
|
|
|
$query = $this->buildJoinQuery($target, $query, 'LEFT', array("$parentAlias.$parentKey" => "$alias.$targetKey"), $options); |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$query['recursive'] = -1; |
152
|
|
|
$query['contain'] = false; |
153
|
|
|
|
154
|
|
|
return $query; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Fetches meta data |
159
|
|
|
* |
160
|
|
|
* @param string $path Path of the association |
161
|
|
|
* @return array |
162
|
|
|
*/ |
163
|
|
|
private function metas($path) { // @codingStandardsIgnoreLine |
164
|
|
|
if (isset($this->metas[$path])) { |
165
|
|
|
return $this->metas[$path]; |
166
|
|
|
} |
167
|
|
|
return array(); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Fetches external associations |
172
|
|
|
* |
173
|
|
|
* @param Model $model Model |
174
|
|
|
* @param string $path The target path of the external primary model, such as 'User.Article' |
175
|
|
|
* @param array $results The results of the parent model |
176
|
|
|
* @return array |
177
|
|
|
*/ |
178
|
|
|
protected function loadExternal(Model $model, $path, array $results) { // @codingStandardsIgnoreLine |
179
|
|
|
if ($results) { |
180
|
|
|
foreach ($this->metas($path) as $meta) { |
181
|
|
|
extract($meta); |
182
|
|
|
if ($external) { |
183
|
|
|
$results = $this->mergeExternalExternal($model, $results, $meta); |
184
|
|
|
} else { |
185
|
|
|
$results = $this->mergeInternalExternal($model, $results, $meta); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
return $results; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Merges results of external associations of an external association |
194
|
|
|
* |
195
|
|
|
* @param Model $model Model |
196
|
|
|
* @param array $results Results |
197
|
|
|
* @param array $meta Meta data to be used for eager loading |
198
|
|
|
* @return array |
199
|
|
|
*/ |
200
|
|
|
private function mergeExternalExternal(Model $model, array $results, array $meta) { // @codingStandardsIgnoreLine |
|
|
|
|
201
|
|
|
extract($meta); |
202
|
|
|
|
203
|
|
|
$db = $target->getDataSource(); |
204
|
|
|
|
205
|
|
|
$assocAlias = $alias; |
206
|
|
|
$assocKey = $targetKey; |
207
|
|
|
|
208
|
|
|
$options = $this->attachAssociations($target, $aliasPath, $options); |
209
|
|
|
if ($has && $belong) { |
210
|
|
|
$assocAlias = $habtmAlias; |
211
|
|
|
$assocKey = $habtmParentKey; |
212
|
|
|
|
213
|
|
|
$options = $this->buildJoinQuery($habtm, $options, 'INNER', array( |
214
|
|
|
"$alias.$targetKey" => "$habtmAlias.$habtmTargetKey", |
215
|
|
|
), $options); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
$options = $this->addField($options, "$assocAlias.$assocKey"); |
219
|
|
|
|
220
|
|
|
$ids = Hash::extract($results, "{n}.$parentAlias.$parentKey"); |
221
|
|
|
$ids = array_unique($ids); |
222
|
|
|
|
223
|
|
|
if (!empty($finderQuery)) { |
224
|
|
|
$assocResults = array(); |
225
|
|
|
foreach ($ids as $id) { |
226
|
|
|
$eachQuery = str_replace('{$__cakeID__$}', $db->value($id), $finderQuery); |
227
|
|
|
$eachAssocResults = $db->fetchAll($eachQuery, $target->cacheQueries); |
228
|
|
|
$eachAssocResults = Hash::insert($eachAssocResults, "{n}.EagerLoaderModel.assoc_id", $id); |
229
|
|
|
$assocResults = array_merge($assocResults, $eachAssocResults); |
230
|
|
|
} |
231
|
|
|
} elseif ($this->hasLimitOffset($options)) { |
232
|
|
|
$assocResults = array(); |
233
|
|
|
foreach ($ids as $id) { |
234
|
|
|
$eachOptions = $options; |
235
|
|
|
$eachOptions['conditions'][] = array("$assocAlias.$assocKey" => $id); |
236
|
|
|
$eachAssocResults = $db->read($target, $eachOptions); |
237
|
|
|
$eachAssocResults = Hash::insert($eachAssocResults, "{n}.EagerLoaderModel.assoc_id", $id); |
238
|
|
|
$assocResults = array_merge($assocResults, $eachAssocResults); |
239
|
|
|
} |
240
|
|
|
} else { |
241
|
|
|
$options['fields'][] = '(' . $db->name($assocAlias . '.' . $assocKey) . ') AS ' . $db->name('EagerLoaderModel' . '__' . 'assoc_id'); |
242
|
|
|
$options['conditions'][] = array("$assocAlias.$assocKey" => $ids); |
243
|
|
|
$assocResults = $db->read($target, $options); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$assocResults = $this->filterResults($parent, $alias, $assocResults); |
247
|
|
|
$assocResults = $this->loadExternal($target, $aliasPath, $assocResults); |
248
|
|
|
|
249
|
|
|
if ($has && $belong) { |
250
|
|
|
foreach ($assocResults as &$assocResult) { |
251
|
|
|
$assocResult[$alias][$habtmAlias] = $assocResult[$habtmAlias]; |
252
|
|
|
unset($assocResult[$habtmAlias]); |
253
|
|
|
} |
254
|
|
|
unset($assocResult); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
foreach ($results as &$result) { |
258
|
|
|
if (!isset($result[$parentAlias][$parentKey])) { |
259
|
|
|
continue; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$assoc = array(); |
263
|
|
|
foreach ($assocResults as $assocResult) { |
264
|
|
|
if ((string)$result[$parentAlias][$parentKey] === (string)$assocResult['EagerLoaderModel']['assoc_id']) { |
265
|
|
|
$assoc[] = $assocResult[$alias]; |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
if (!$many) { |
269
|
|
|
$assoc = $assoc ? current($assoc) : array(); |
270
|
|
|
} |
271
|
|
|
$result = $this->mergeAssocResult($result, $assoc, $propertyPath); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
return $results; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Merges results of external associations of an internal association |
279
|
|
|
* |
280
|
|
|
* @param Model $model Model |
281
|
|
|
* @param array $results Results |
282
|
|
|
* @param array $meta Meta data to be used for eager loading |
283
|
|
|
* @return array |
284
|
|
|
*/ |
285
|
|
|
private function mergeInternalExternal(Model $model, array $results, array $meta) { // @codingStandardsIgnoreLine |
286
|
|
|
extract($meta); |
287
|
|
|
|
288
|
|
|
$assocResults = array(); |
289
|
|
|
foreach ($results as $n => &$result) { |
290
|
|
|
if ($result[$alias][$targetKey] === null) { |
291
|
|
|
// Remove NULL association created by LEFT JOIN |
292
|
|
|
if (empty($eager)) { |
293
|
|
|
$assocResults[$n] = array( $alias => array() ); |
294
|
|
|
} |
295
|
|
|
} else { |
296
|
|
|
$assocResults[$n] = array( $alias => $result[$alias] ); |
297
|
|
|
} |
298
|
|
|
unset($result[$alias]); |
299
|
|
|
} |
300
|
|
|
unset($result); |
301
|
|
|
|
302
|
|
|
if (!empty($eager) && !isset($model->$alias)) { |
303
|
|
|
$assocResults = $this->filterResults($parent, $alias, $assocResults); |
304
|
|
|
} |
305
|
|
|
$assocResults = $this->loadExternal($target, $aliasPath, $assocResults); |
306
|
|
|
|
307
|
|
|
foreach ($results as $n => &$result) { |
308
|
|
|
if (isset($assocResults[$n][$alias])) { |
309
|
|
|
$assoc = $assocResults[$n][$alias]; |
310
|
|
|
$result = $this->mergeAssocResult($result, $assoc, $propertyPath); |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
unset($result); |
314
|
|
|
|
315
|
|
|
return $results; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Merges associated result |
320
|
|
|
* |
321
|
|
|
* @param array $result Results |
322
|
|
|
* @param array $assoc Associated results |
323
|
|
|
* @param string $propertyPath Path of the results |
324
|
|
|
* @return array |
325
|
|
|
*/ |
326
|
|
|
private function mergeAssocResult(array $result, array $assoc, $propertyPath) { // @codingStandardsIgnoreLine |
327
|
|
|
return Hash::insert($result, $propertyPath, $assoc + (array)Hash::get($result, $propertyPath)); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Reformat `contain` array |
332
|
|
|
* |
333
|
|
|
* @param array|string $contain The value of `contain` option of the query |
334
|
|
|
* @return array |
335
|
|
|
*/ |
336
|
|
|
private function reformatContain($contain) { // @codingStandardsIgnoreLine |
337
|
|
|
$result = array( |
338
|
|
|
'options' => array(), |
339
|
|
|
'contain' => array(), |
340
|
|
|
); |
341
|
|
|
|
342
|
|
|
$contain = (array)$contain; |
343
|
|
|
foreach ($contain as $key => $val) { |
344
|
|
|
if (is_int($key)) { |
345
|
|
|
$key = $val; |
346
|
|
|
$val = array(); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
if (!isset($this->containOptions[$key])) { |
350
|
|
|
if (strpos($key, '.') !== false) { |
351
|
|
|
$expanded = Hash::expand(array($key => $val)); |
352
|
|
|
list($key, $val) = each($expanded); |
353
|
|
|
} |
354
|
|
|
$ref =& $result['contain'][$key]; |
355
|
|
|
$ref = Hash::merge((array)$ref, $this->reformatContain($val)); |
356
|
|
|
} else { |
357
|
|
|
$result['options'][$key] = $val; |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
return $result; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Normalizes the query |
366
|
|
|
* |
367
|
|
|
* @param Model $model Model |
368
|
|
|
* @param array $query Query |
369
|
|
|
* @return array Normalized query |
370
|
|
|
*/ |
371
|
|
|
private function normalizeQuery(Model $model, array $query) { // @codingStandardsIgnoreLine |
372
|
|
|
$db = $model->getDataSource(); |
373
|
|
|
|
374
|
|
|
$query += array( |
375
|
|
|
'fields' => array(), |
376
|
|
|
'conditions' => array(), |
377
|
|
|
'order' => array() |
378
|
|
|
); |
379
|
|
|
|
380
|
|
|
if (!$query['fields']) { |
381
|
|
|
$query['fields'] = $db->fields($model, null, array(), false); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
$query['fields'] = (array)$query['fields']; |
385
|
|
|
foreach ($query['fields'] as &$field) { |
386
|
|
|
if ($model->isVirtualField($field)) { |
387
|
|
|
$fields = $db->fields($model, null, array($field), false); |
388
|
|
|
$field = $fields[0]; |
389
|
|
|
} else { |
390
|
|
|
$field = $this->normalizeField($model, $field); |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
unset($field); |
394
|
|
|
|
395
|
|
|
$query['conditions'] = (array)$query['conditions']; |
396
|
|
|
foreach ($query['conditions'] as $key => $val) { |
397
|
|
|
if ($model->hasField($key)) { |
398
|
|
|
unset($query['conditions'][$key]); |
399
|
|
|
$key = $this->normalizeField($model, $key); |
400
|
|
|
$query['conditions'][] = array($key => $val); |
401
|
|
|
} elseif ($model->isVirtualField($key)) { |
402
|
|
|
unset($query['conditions'][$key]); |
403
|
|
|
$conditions = $db->conditionKeysToString(array($key => $val), true, $model); |
404
|
|
|
$query['conditions'][] = $db->expression($conditions[0]); |
405
|
|
|
} |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
$order = array(); |
409
|
|
|
foreach ((array)$query['order'] as $key => $val) { |
410
|
|
|
if (is_int($key)) { |
411
|
|
|
$val = $this->normalizeField($model, $val); |
412
|
|
|
} else { |
413
|
|
|
$key = $this->normalizeField($model, $key); |
414
|
|
|
} |
415
|
|
|
$order += array($key => $val); |
416
|
|
|
} |
417
|
|
|
$query['order'] = $order; |
418
|
|
|
|
419
|
|
|
return $query; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Normalize field |
424
|
|
|
* |
425
|
|
|
* @param Model $model Model |
426
|
|
|
* @param string $field Name of the field |
427
|
|
|
* @return string |
428
|
|
|
*/ |
429
|
|
|
private function normalizeField(Model $model, $field) { // @codingStandardsIgnoreLine |
430
|
|
|
if ($model->hasField($field)) { |
431
|
|
|
$field = $model->alias . '.' . $field; |
432
|
|
|
} elseif ($model->isVirtualField($field)) { |
433
|
|
|
$db = $model->getDataSource(); |
434
|
|
|
$field = $model->getVirtualField($field); |
435
|
|
|
$field = $db->dispatchMethod('_quoteFields', array($field)); |
436
|
|
|
$field = '(' . $field . ')'; |
437
|
|
|
} |
438
|
|
|
return $field; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Modifies the query to apply joins. |
443
|
|
|
* |
444
|
|
|
* @param Model $target Model to be joined |
445
|
|
|
* @param array $query Query |
446
|
|
|
* @param string $joinType The type for join |
447
|
|
|
* @param array $keys Key fields being used for join |
448
|
|
|
* @param array $options Extra options for join |
449
|
|
|
* @return array Modified query |
450
|
|
|
*/ |
451
|
|
|
private function buildJoinQuery(Model $target, array $query, $joinType, array $keys, array $options) { // @codingStandardsIgnoreLine |
452
|
|
|
$db = $target->getDataSource(); |
453
|
|
|
|
454
|
|
|
$options = $this->normalizeQuery($target, $options); |
455
|
|
|
$query['fields'] = array_merge($query['fields'], $options['fields']); |
456
|
|
|
$query = $this->normalizeQuery($target, $query); |
457
|
|
|
|
458
|
|
|
foreach ($keys as $lhs => $rhs) { |
459
|
|
|
$query = $this->addField($query, $lhs); |
460
|
|
|
$query = $this->addField($query, $rhs); |
461
|
|
|
$options['conditions'][] = array($lhs => $db->identifier($rhs)); |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
$query['joins'][] = array( |
465
|
|
|
'type' => $joinType, |
466
|
|
|
'table' => $target, |
467
|
|
|
'alias' => $target->alias, |
468
|
|
|
'conditions' => $options['conditions'], |
469
|
|
|
); |
470
|
|
|
return $query; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Adds a field into the `fields` option of the query |
475
|
|
|
* |
476
|
|
|
* @param array $query Query |
477
|
|
|
* @param string $field Name of the field |
478
|
|
|
* @return Modified query |
479
|
|
|
*/ |
480
|
|
|
private function addField(array $query, $field) { // @codingStandardsIgnoreLine |
481
|
|
|
if (!in_array($field, $query['fields'], true)) { |
482
|
|
|
$query['fields'][] = $field; |
483
|
|
|
} |
484
|
|
|
return $query; |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Parse the `contain` option of the query recursively |
489
|
|
|
* |
490
|
|
|
* @param Model $parent Parent model of the contained model |
491
|
|
|
* @param string $alias Alias of the contained model |
492
|
|
|
* @param array $contain Reformatted `contain` option for the deep associations |
493
|
|
|
* @param array|null $context Context |
494
|
|
|
* @return array |
495
|
|
|
* @throws InvalidArgumentException |
496
|
|
|
*/ |
497
|
|
|
private function parseContain(Model $parent, $alias, array $contain, $context = null) { // @codingStandardsIgnoreLine |
498
|
|
|
if ($context === null) { |
499
|
|
|
$context = array( |
500
|
|
|
'root' => $parent->alias, |
501
|
|
|
'aliasPath' => $parent->alias, |
502
|
|
|
'propertyPath' => '', |
503
|
|
|
'forceExternal' => false, |
504
|
|
|
); |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
$aliasPath = $context['aliasPath'] . '.' . $alias; |
508
|
|
|
$propertyPath = ($context['propertyPath'] ? $context['propertyPath'] . '.' : '') . $alias; |
509
|
|
|
|
510
|
|
|
$types = $parent->getAssociated(); |
511
|
|
|
if (!isset($types[$alias])) { |
512
|
|
|
throw new InvalidArgumentException(sprintf('Model "%s" is not associated with model "%s"', $parent->alias, $alias), E_USER_WARNING); |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
$parentAlias = $parent->alias; |
516
|
|
|
$target = $parent->$alias; |
517
|
|
|
$type = $types[$alias]; |
518
|
|
|
$relation = $parent->{$type}[$alias]; |
519
|
|
|
|
520
|
|
|
$options = $contain['options'] + array_intersect_key(Hash::filter($relation), $this->containOptions); |
521
|
|
|
|
522
|
|
|
$has = (stripos($type, 'has') !== false); |
523
|
|
|
$many = (stripos($type, 'many') !== false); |
524
|
|
|
$belong = (stripos($type, 'belong') !== false); |
525
|
|
|
|
526
|
|
|
if ($has && $belong) { |
527
|
|
|
$parentKey = $parent->primaryKey; |
528
|
|
|
$targetKey = $target->primaryKey; |
529
|
|
|
$habtmAlias = $relation['with']; |
530
|
|
|
$habtm = $parent->$habtmAlias; |
531
|
|
|
$habtmParentKey = $relation['foreignKey']; |
532
|
|
|
$habtmTargetKey = $relation['associationForeignKey']; |
533
|
|
|
} elseif ($has) { |
534
|
|
|
$parentKey = $parent->primaryKey; |
535
|
|
|
$targetKey = $relation['foreignKey']; |
536
|
|
|
} else { |
537
|
|
|
$parentKey = $relation['foreignKey']; |
538
|
|
|
$targetKey = $target->primaryKey; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
if (!empty($relation['external'])) { |
542
|
|
|
$external = true; |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
if (!empty($relation['finderQuery'])) { |
546
|
|
|
$finderQuery = $relation['finderQuery']; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
$meta = compact( |
550
|
|
|
'alias', 'parent', 'target', |
551
|
|
|
'parentAlias', 'parentKey', |
552
|
|
|
'targetKey', 'aliasPath', 'propertyPath', |
553
|
|
|
'options', 'has', 'many', 'belong', 'external', 'finderQuery', |
554
|
|
|
'habtm', 'habtmAlias', 'habtmParentKey', 'habtmTargetKey' |
555
|
|
|
); |
556
|
|
|
|
557
|
|
|
if ($this->isExternal($context, $meta)) { |
558
|
|
|
$meta['propertyPath'] = ($context['propertyPath'] ? $parentAlias . '.' : '') . $alias; |
559
|
|
|
$meta['external'] = true; |
560
|
|
|
|
561
|
|
|
$context['root'] = $aliasPath; |
562
|
|
|
$context['propertyPath'] = $alias; |
563
|
|
|
|
564
|
|
|
$path = $context['aliasPath']; |
565
|
|
|
} else { |
566
|
|
|
$meta['external'] = false; |
567
|
|
|
if ($context['root'] !== $context['aliasPath']) { |
568
|
|
|
$meta['eager'] = true; |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
$context['propertyPath'] = $propertyPath; |
572
|
|
|
|
573
|
|
|
$path = $context['root']; |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
$this->metas[$path][] = $meta; |
577
|
|
|
|
578
|
|
|
$context['aliasPath'] = $aliasPath; |
579
|
|
|
$context['forceExternal'] = !empty($finderQuery); |
580
|
|
|
|
581
|
|
|
foreach ($contain['contain'] as $key => $val) { |
582
|
|
|
$this->parseContain($target, $key, $val, $context); |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
return $this->metas; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Returns whether the target is external or not |
590
|
|
|
* |
591
|
|
|
* @param array $context Context |
592
|
|
|
* @param array $meta Meta data to be used for eager loading |
593
|
|
|
* @return bool |
594
|
|
|
*/ |
595
|
|
|
private function isExternal(array $context, array $meta) { // @codingStandardsIgnoreLine |
596
|
|
|
extract($meta); |
597
|
|
|
|
598
|
|
|
if ($parent->useDbConfig !== $target->useDbConfig) { |
599
|
|
|
return true; |
600
|
|
|
} |
601
|
|
|
if (!empty($external)) { |
602
|
|
|
return true; |
603
|
|
|
} |
604
|
|
|
if (!empty($many)) { |
605
|
|
|
return true; |
606
|
|
|
} |
607
|
|
|
if (!empty($finderQuery)) { |
608
|
|
|
return true; |
609
|
|
|
} |
610
|
|
|
if ($this->hasLimitOffset($options)) { |
611
|
|
|
return true; |
612
|
|
|
} |
613
|
|
|
if ($context['forceExternal']) { |
614
|
|
|
return true; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
$metas = $this->metas($context['root']); |
618
|
|
|
$aliases = Hash::extract($metas, '{n}.alias'); |
619
|
|
|
if (in_array($alias, $aliases, true)) { |
620
|
|
|
return true; |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
return false; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* Returns where `limit` or `offset` option exists |
628
|
|
|
* |
629
|
|
|
* @param array $options Options |
630
|
|
|
* @return bool |
631
|
|
|
*/ |
632
|
|
|
private function hasLimitOffset($options) { // @codingStandardsIgnoreLine |
633
|
|
|
return !empty($options['limit']) || !empty($options['offset']); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Triggers afterFind() method |
638
|
|
|
* |
639
|
|
|
* @param Model $parent Model |
640
|
|
|
* @param string $alias Alias |
641
|
|
|
* @param array $results Results |
642
|
|
|
* @return array |
643
|
|
|
*/ |
644
|
|
|
private function filterResults(Model $parent, $alias, array $results) { // @codingStandardsIgnoreLine |
645
|
|
|
$db = $parent->getDataSource(); |
|
|
|
|
646
|
|
|
|
647
|
|
|
$target = $parent->$alias; |
648
|
|
|
|
649
|
|
|
foreach ($results as $key => &$result) { |
650
|
|
|
$data = $target->afterFind(array(array($alias => $result[$alias])), false); |
651
|
|
|
if (isset($data[0][$alias])) { |
652
|
|
|
$result[$alias] = $data[0][$alias]; |
653
|
|
|
} else { |
654
|
|
|
unset($results[$key]); |
655
|
|
|
} |
656
|
|
|
} |
657
|
|
|
|
658
|
|
|
return $results; |
659
|
|
|
} |
660
|
|
|
} |
661
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.