Completed
Push — master ( 023f57...8986d5 )
by Erick
01:12
created

EagerLoader::getJoinType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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
113
		return $query;
114
	}
115
116
/**
117
 * Modifies the results
118
 *
119
 * @param Model $model Model
120
 * @param array $results Results
121
 * @return array Modified results
122
 */
123
	private  function transformResults(Model $model, array $results) { // @codingStandardsIgnoreLine
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
124
		foreach ($results as &$result) {
125
			unset($result['EagerLoaderModel']);
126
		}
127
		return $this->loadExternal($model, $model->alias, $results);
128
	}
129
130
/**
131
 * Modifies the query to fetch attachable associations.
132
 *
133
 * @param Model $model Model
134
 * @param string $path The target path of the model, such as 'User.Article'
135
 * @param array $query Query
136
 * @return array Modified query
137
 */
138
	private function attachAssociations(Model $model, $path, array $query) { // @codingStandardsIgnoreLine
139
		$query = $this->normalizeQuery($model, $query);
140
141
		foreach ($this->metas($path) as $meta) {
142
			extract($meta);
143
			if ($external) {
144
				$query = $this->addField($query, "$parentAlias.$parentKey");
145
			} else {
146
				$joinType = $this->getJoinType($parent, $target, $type);
147
				$query = $this->buildJoinQuery($target, $query, $joinType, array("$parentAlias.$parentKey" => "$alias.$targetKey"), $options);
148
			}
149
		}
150
151
		$query['recursive'] = -1;
152
		$query['contain'] = false;
153
154
		return $query;
155
	}
156
157
	private function getJoinType(Model $parent, Model $child, $associationType){
158
			return $parent->{$associationType}[$child->alias]['type'] ?? 'LEFT';
159
	}
160
161
/**
162
 * Fetches meta data
163
 *
164
 * @param string $path Path of the association
165
 * @return array
166
 */
167
	private function metas($path) { // @codingStandardsIgnoreLine
168
		if (isset($this->metas[$path])) {
169
			return $this->metas[$path];
170
		}
171
		return array();
172
	}
173
174
/**
175
 * Fetches external associations
176
 *
177
 * @param Model $model Model
178
 * @param string $path The target path of the external primary model, such as 'User.Article'
179
 * @param array $results The results of the parent model
180
 * @return array
181
 */
182
	protected function loadExternal(Model $model, $path, array $results) { // @codingStandardsIgnoreLine
183
		if ($results) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
184
			foreach ($this->metas($path) as $meta) {
185
				extract($meta);
186
				if ($external) {
187
					$results = $this->mergeExternalExternal($model, $results, $meta);
188
				} else {
189
					$results = $this->mergeInternalExternal($model, $results, $meta);
190
				}
191
			}
192
		}
193
		return $results;
194
	}
195
196
/**
197
 * Merges results of external associations of an external association
198
 *
199
 * @param Model $model Model
200
 * @param array $results Results
201
 * @param array $meta Meta data to be used for eager loading
202
 * @return array
203
 */
204
	private function mergeExternalExternal(Model $model, array $results, array $meta) { // @codingStandardsIgnoreLine
0 ignored issues
show
Unused Code introduced by
The parameter $model is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
205
		extract($meta);
206
207
		$db = $target->getDataSource();
208
209
		$assocAlias = $alias;
210
		$assocKey = $targetKey;
211
212
		$options = $this->attachAssociations($target, $aliasPath, $options);
213
		if ($has && $belong) {
214
			$assocAlias = $habtmAlias;
215
			$assocKey = $habtmParentKey;
216
217
			$options = $this->buildJoinQuery($habtm, $options, 'INNER', array(
218
				"$alias.$targetKey" => "$habtmAlias.$habtmTargetKey",
219
			), $options);
220
		}
221
222
		$options = $this->addField($options, "$assocAlias.$assocKey");
223
224
		$ids = Hash::extract($results, "{n}.$parentAlias.$parentKey");
225
		$ids = array_unique($ids);
226
227
		if (!empty($finderQuery)) {
228
			$assocResults = array();
229
			foreach ($ids as $id) {
230
				$eachQuery = str_replace('{$__cakeID__$}', $db->value($id), $finderQuery);
231
				$eachAssocResults = $db->fetchAll($eachQuery, $target->cacheQueries);
232
				$eachAssocResults = Hash::insert($eachAssocResults, "{n}.EagerLoaderModel.assoc_id", $id);
233
				$assocResults = array_merge($assocResults, $eachAssocResults);
234
			}
235
		} elseif ($this->hasLimitOffset($options)) {
236
			$assocResults = array();
237
			foreach ($ids as $id) {
238
				$eachOptions = $options;
239
				$eachOptions['conditions'][] = array("$assocAlias.$assocKey" => $id);
240
				$eachAssocResults = $db->read($target, $eachOptions);
241
				$eachAssocResults = Hash::insert($eachAssocResults, "{n}.EagerLoaderModel.assoc_id", $id);
242
				$assocResults = array_merge($assocResults, $eachAssocResults);
243
			}
244
		} else {
245
			$options['fields'][] = '(' . $db->name($assocAlias . '.' . $assocKey) . ') AS ' . $db->name('EagerLoaderModel' . '__' . 'assoc_id');
246
			$options['conditions'][] = array("$assocAlias.$assocKey" => $ids);
247
			$assocResults = $db->read($target, $options);
248
		}
249
250
		$assocResults = $this->filterResults($parent, $alias, $assocResults);
251
		$assocResults = $this->loadExternal($target, $aliasPath, $assocResults);
252
253
		if ($has && $belong) {
254
			foreach ($assocResults as &$assocResult) {
255
				$assocResult[$alias][$habtmAlias] = $assocResult[$habtmAlias];
256
				unset($assocResult[$habtmAlias]);
257
			}
258
			unset($assocResult);
259
		}
260
261
		foreach ($results as &$result) {
262
			if (!isset($result[$parentAlias][$parentKey])) {
263
				continue;
264
			}
265
266
			$assoc = array();
267
			foreach ($assocResults as $assocResult) {
268
				if ((string)$result[$parentAlias][$parentKey] === (string)$assocResult['EagerLoaderModel']['assoc_id']) {
269
					$assoc[] = $assocResult[$alias];
270
				}
271
			}
272
			if (!$many) {
273
				$assoc = $assoc ? current($assoc) : array();
274
			}
275
			$result = $this->mergeAssocResult($result, $assoc, $propertyPath);
276
		}
277
278
		return $results;
279
	}
280
281
/**
282
 * Merges results of external associations of an internal association
283
 *
284
 * @param Model $model Model
285
 * @param array $results Results
286
 * @param array $meta Meta data to be used for eager loading
287
 * @return array
288
 */
289
	private function mergeInternalExternal(Model $model, array $results, array $meta) { // @codingStandardsIgnoreLine
290
		extract($meta);
291
292
		$assocResults = array();
293
		foreach ($results as $n => &$result) {
294
			if ($result[$alias][$targetKey] === null) {
295
				// Remove NULL association created by LEFT JOIN
296
				if (empty($eager)) {
297
					$assocResults[$n] = array( $alias => array() );
298
				}
299
			} else {
300
				$assocResults[$n] = array( $alias => $result[$alias] );
301
			}
302
			unset($result[$alias]);
303
		}
304
		unset($result);
305
306
		if (!empty($eager) && !isset($model->$alias)) {
307
			$assocResults = $this->filterResults($parent, $alias, $assocResults);
308
		}
309
		$assocResults = $this->loadExternal($target, $aliasPath, $assocResults);
310
311
		foreach ($results as $n => &$result) {
312
			if (isset($assocResults[$n][$alias])) {
313
				$assoc = $assocResults[$n][$alias];
314
				$result = $this->mergeAssocResult($result, $assoc, $propertyPath);
315
			}
316
		}
317
		unset($result);
318
319
		return $results;
320
	}
321
322
/**
323
 * Merges associated result
324
 *
325
 * @param array $result Results
326
 * @param array $assoc Associated results
327
 * @param string $propertyPath Path of the results
328
 * @return array
329
 */
330
	private function mergeAssocResult(array $result, array $assoc, $propertyPath) { // @codingStandardsIgnoreLine
331
		return Hash::insert($result, $propertyPath, $assoc + (array)Hash::get($result, $propertyPath));
332
	}
333
334
/**
335
 * Reformat `contain` array
336
 *
337
 * @param array|string $contain The value of `contain` option of the query
338
 * @return array
339
 */
340
	private function reformatContain($contain) { // @codingStandardsIgnoreLine
341
		$result = array(
342
			'options' => array(),
343
			'contain' => array(),
344
		);
345
346
		$contain = (array)$contain;
347
		foreach ($contain as $key => $val) {
348
			if (is_int($key)) {
349
				$key = $val;
350
				$val = array();
351
			}
352
353
			if (!isset($this->containOptions[$key])) {
354
				if (strpos($key, '.') !== false) {
355
					$expanded = Hash::expand(array($key => $val));
356
					list($key, $val) = each($expanded);
357
				}
358
				$ref =& $result['contain'][$key];
359
				$ref = Hash::merge((array)$ref, $this->reformatContain($val));
360
			} else {
361
				$result['options'][$key] = $val;
362
			}
363
		}
364
365
		return $result;
366
	}
367
368
/**
369
 * Normalizes the query
370
 *
371
 * @param Model $model Model
372
 * @param array $query Query
373
 * @return array Normalized query
374
 */
375
	private function normalizeQuery(Model $model, array $query) { // @codingStandardsIgnoreLine
376
		$db = $model->getDataSource();
377
378
		$query += array(
379
			'fields' => array(),
380
			'conditions' => array(),
381
			'order' => array()
382
		);
383
384
		if (!$query['fields']) {
385
			$query['fields'] = $db->fields($model, null, array(), false);
386
		}
387
388
		$query['fields'] = (array)$query['fields'];
389
		foreach ($query['fields'] as &$field) {
390
			if ($model->isVirtualField($field)) {
391
				$fields = $db->fields($model, null, array($field), false);
392
				$field = $fields[0];
393
			} else {
394
				$field = $this->normalizeField($model, $field);
395
			}
396
		}
397
		unset($field);
398
399
		$query['conditions'] = (array)$query['conditions'];
400
		foreach ($query['conditions'] as $key => $val) {
401
			if ($model->hasField($key)) {
402
				unset($query['conditions'][$key]);
403
				$key = $this->normalizeField($model, $key);
404
				$query['conditions'][] = array($key => $val);
405
			} elseif ($model->isVirtualField($key)) {
406
				unset($query['conditions'][$key]);
407
				$conditions = $db->conditionKeysToString(array($key => $val), true, $model);
408
				$query['conditions'][] = $db->expression($conditions[0]);
409
			}
410
		}
411
412
		$order = array();
413
		foreach ((array)$query['order'] as $key => $val) {
414
			if (is_int($key)) {
415
				$val = $this->normalizeField($model, $val);
416
			} else {
417
				$key = $this->normalizeField($model, $key);
418
			}
419
			$order += array($key => $val);
420
		}
421
		$query['order'] = $order;
422
423
		return $query;
424
	}
425
426
/**
427
 * Normalize field
428
 *
429
 * @param Model $model Model
430
 * @param string $field Name of the field
431
 * @return string
432
 */
433
	private function normalizeField(Model $model, $field) { // @codingStandardsIgnoreLine
434
		if ($model->hasField($field)) {
435
			$field = $model->alias . '.' . $field;
436
		} elseif ($model->isVirtualField($field)) {
437
			$db = $model->getDataSource();
438
			$field = $model->getVirtualField($field);
439
			$field = $db->dispatchMethod('_quoteFields', array($field));
440
			$field = '(' . $field . ')';
441
		}
442
		return $field;
443
	}
444
445
/**
446
 * Modifies the query to apply joins.
447
 *
448
 * @param Model $target Model to be joined
449
 * @param array $query Query
450
 * @param string $joinType The type for join
451
 * @param array $keys Key fields being used for join
452
 * @param array $options Extra options for join
453
 * @return array Modified query
454
 */
455
	private function buildJoinQuery(Model $target, array $query, $joinType, array $keys, array $options) { // @codingStandardsIgnoreLine
456
		$db = $target->getDataSource();
457
458
		$options = $this->normalizeQuery($target, $options);
459
		$query['fields'] = array_merge($query['fields'], $options['fields']);
460
		$query = $this->normalizeQuery($target, $query);
461
462
		foreach ($keys as $lhs => $rhs) {
463
			$query = $this->addField($query, $lhs);
464
			$query = $this->addField($query, $rhs);
465
			$options['conditions'][] = array($lhs => $db->identifier($rhs));
466
		}
467
468
		$query['joins'][] = array(
469
			'type' => $joinType,
470
			'table' => $target,
471
			'alias' => $target->alias,
472
			'conditions' => $options['conditions'],
473
		);
474
		return $query;
475
	}
476
477
/**
478
 * Adds a field into the `fields` option of the query
479
 *
480
 * @param array $query Query
481
 * @param string $field Name of the field
482
 * @return Modified query
483
 */
484
	private function addField(array $query, $field) { // @codingStandardsIgnoreLine
485
		if (!in_array($field, $query['fields'], true)) {
486
			$query['fields'][] = $field;
487
		}
488
		return $query;
489
	}
490
491
/**
492
 * Parse the `contain` option of the query recursively
493
 *
494
 * @param Model $parent Parent model of the contained model
495
 * @param string $alias Alias of the contained model
496
 * @param array $contain Reformatted `contain` option for the deep associations
497
 * @param array|null $context Context
498
 * @return array
499
 * @throws InvalidArgumentException
500
 */
501
	private function parseContain(Model $parent, $alias, array $contain, $context = null) { // @codingStandardsIgnoreLine
502
		if ($context === null) {
503
			$context = array(
504
				'root' => $parent->alias,
505
				'aliasPath' => $parent->alias,
506
				'propertyPath' => '',
507
				'forceExternal' => false,
508
			);
509
		}
510
511
		$aliasPath = $context['aliasPath'] . '.' . $alias;
512
		$propertyPath = ($context['propertyPath'] ? $context['propertyPath'] . '.' : '') . $alias;
513
514
		$types = $parent->getAssociated();
515
		if (!isset($types[$alias])) {
516
			throw new InvalidArgumentException(sprintf('Model "%s" is not associated with model "%s"', $parent->alias, $alias), E_USER_WARNING);
517
		}
518
519
		$parentAlias = $parent->alias;
520
		$target = $parent->$alias;
521
		$type = $types[$alias];
522
		$relation = $parent->{$type}[$alias];
523
524
		$options = $contain['options'] + array_intersect_key(Hash::filter($relation), $this->containOptions);
525
526
		$has = (stripos($type, 'has') !== false);
527
		$many = (stripos($type, 'many') !== false);
528
		$belong = (stripos($type, 'belong') !== false);
529
530
		if ($has && $belong) {
531
			$parentKey = $parent->primaryKey;
532
			$targetKey = $target->primaryKey;
533
			$habtmAlias = $relation['with'];
534
			$habtm = $parent->$habtmAlias;
535
			$habtmParentKey = $relation['foreignKey'];
536
			$habtmTargetKey = $relation['associationForeignKey'];
537
		} elseif ($has) {
538
			$parentKey = $parent->primaryKey;
539
			$targetKey = $relation['foreignKey'];
540
		} else {
541
			$parentKey = $relation['foreignKey'];
542
			$targetKey = $target->primaryKey;
543
		}
544
545
		if (!empty($relation['external'])) {
546
			$external = true;
547
		}
548
549
		if (!empty($relation['finderQuery'])) {
550
			$finderQuery = $relation['finderQuery'];
551
		}
552
553
		$meta = compact(
554
			'alias', 'parent', 'target',
555
			'parentAlias', 'parentKey',
556
			'targetKey', 'aliasPath', 'propertyPath',
557
			'options', 'has', 'many', 'belong', 'external', 'finderQuery',
558
			'habtm', 'habtmAlias', 'habtmParentKey', 'habtmTargetKey', 'type'
559
		);
560
561
		if ($this->isExternal($context, $meta)) {
562
			$meta['propertyPath'] = ($context['propertyPath'] ? $parentAlias . '.' : '') . $alias;
563
			$meta['external'] = true;
564
565
			$context['root'] = $aliasPath;
566
			$context['propertyPath'] = $alias;
567
568
			$path = $context['aliasPath'];
569
		} else {
570
			$meta['external'] = false;
571
			if ($context['root'] !== $context['aliasPath']) {
572
				$meta['eager'] = true;
573
			}
574
575
			$context['propertyPath'] = $propertyPath;
576
577
			$path = $context['root'];
578
		}
579
580
		$this->metas[$path][] = $meta;
581
582
		$context['aliasPath'] = $aliasPath;
583
		$context['forceExternal'] = !empty($finderQuery);
584
585
		foreach ($contain['contain'] as $key => $val) {
586
			$this->parseContain($target, $key, $val, $context);
587
		}
588
589
		return $this->metas;
590
	}
591
592
/**
593
 * Returns whether the target is external or not
594
 *
595
 * @param array $context Context
596
 * @param array $meta Meta data to be used for eager loading
597
 * @return bool
598
 */
599
	private function isExternal(array $context, array $meta) { // @codingStandardsIgnoreLine
600
		extract($meta);
601
602
		if ($parent->useDbConfig !== $target->useDbConfig) {
603
			return true;
604
		}
605
		if (!empty($external)) {
606
			return true;
607
		}
608
		if (!empty($many)) {
609
			return true;
610
		}
611
		if (!empty($finderQuery)) {
612
			return true;
613
		}
614
		if ($this->hasLimitOffset($options)) {
615
			return true;
616
		}
617
		if ($context['forceExternal']) {
618
			return true;
619
		}
620
621
		$metas = $this->metas($context['root']);
622
		$aliases = Hash::extract($metas, '{n}.alias');
623
		if (in_array($alias, $aliases, true)) {
624
			return true;
625
		}
626
627
		return false;
628
	}
629
630
/**
631
 * Returns where `limit` or `offset` option exists
632
 *
633
 * @param array $options Options
634
 * @return bool
635
 */
636
	private function hasLimitOffset($options) { // @codingStandardsIgnoreLine
637
		return !empty($options['limit']) || !empty($options['offset']);
638
	}
639
640
/**
641
 * Triggers afterFind() method
642
 *
643
 * @param Model $parent Model
644
 * @param string $alias Alias
645
 * @param array $results Results
646
 * @return array
647
 */
648
	private function filterResults(Model $parent, $alias, array $results) { // @codingStandardsIgnoreLine
649
		$db = $parent->getDataSource();
0 ignored issues
show
Unused Code introduced by
$db is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
650
651
		$target = $parent->$alias;
652
653
		foreach ($results as $key => &$result) {
654
			$data = $target->afterFind(array(array($alias => $result[$alias])), false);
655
			if (isset($data[0][$alias])) {
656
				$result[$alias] = $data[0][$alias];
657
			} else {
658
				unset($results[$key]);
659
			}
660
		}
661
662
		return $results;
663
	}
664
}
665