Criteria::mergeWith()   F
last analyzed

Complexity

Conditions 22
Paths 1633

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 24.48

Importance

Changes 0
Metric Value
dl 0
loc 71
ccs 24
cts 29
cp 0.8276
rs 0
c 0
b 0
f 0
cc 22
nc 1633
nop 1
crap 24.48

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This software package is licensed under AGPL or Commercial license.
5
 *
6
 * @package   maslosoft/mangan
7
 * @licence   AGPL or Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]>
9
 * @copyright Copyright (c) Maslosoft
10
 * @copyright Copyright (c) Others as mentioned in code
11
 * @link      https://maslosoft.com/mangan/
12
 */
13
14
namespace Maslosoft\Mangan;
15
16
use Exception;
17
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
18
use Maslosoft\Mangan\Criteria\ConditionDecorator;
19
use Maslosoft\Mangan\Criteria\Conditions;
20
use Maslosoft\Mangan\Interfaces\Criteria\DecoratableInterface;
21
use Maslosoft\Mangan\Interfaces\Criteria\LimitableInterface;
22
use Maslosoft\Mangan\Interfaces\Criteria\MergeableInterface;
23
use Maslosoft\Mangan\Interfaces\Criteria\SelectableInterface;
24
use Maslosoft\Mangan\Interfaces\Criteria\SortableInterface;
25
use Maslosoft\Mangan\Interfaces\CriteriaInterface;
26
use Maslosoft\Mangan\Interfaces\ModelAwareInterface;
27
use Maslosoft\Mangan\Interfaces\SortInterface;
28
use Maslosoft\Mangan\Traits\Criteria\CursorAwareTrait;
29
use Maslosoft\Mangan\Traits\Criteria\DecoratableTrait;
30
use Maslosoft\Mangan\Traits\Criteria\LimitableTrait;
31
use Maslosoft\Mangan\Traits\Criteria\SelectableTrait;
32
use Maslosoft\Mangan\Traits\Criteria\SortableTrait;
33
use Maslosoft\Mangan\Traits\ModelAwareTrait;
34
use UnexpectedValueException;
35
36
/**
37
 * Criteria
38
 *
39
 * This class is a helper for building MongoDB query arrays, it support three syntaxes for adding conditions:
40
 *
41
 * 1. 'equals' syntax:
42
 *    $criteriaObject->fieldName = $value; // this will produce fieldName == value query
43
 * 2. fieldName call syntax
44
 *    $criteriaObject->fieldName($operator, $value); // this will produce fieldName <operator> value
45
 *    $criteriaObject->fieldName($value); // this will produce fieldName == value
46
 * 3. addCond method
47
 *    $criteriaObject->addCond($fieldName, $operator, $vale); // this will produce fieldName <operator> value
48
 *
49
 * For operators list {@see Criteria::$operators}
50
 *
51
 * @author    Ianaré Sévi
52
 * @author    Dariusz Górecki <[email protected]>
53
 * @author    Invenzzia Group, open-source division of CleverIT company http://www.invenzzia.org
54
 * @copyright 2011 CleverIT http://www.cleverit.com.pl
55
 * @license   New BSD license
56
 */
57
class Criteria implements CriteriaInterface,
58
	ModelAwareInterface
59
{
60
61
	use CursorAwareTrait,
62
		DecoratableTrait,
63
		LimitableTrait,
64
		ModelAwareTrait,
65
		SelectableTrait,
66
		SortableTrait;
67
68
	/**
69
	 * @since v1.0
70
	 * @var array $operators supported operators lists
71
	 */
72
	public static $operators = [
73
		// Comparison
74
		// Matches values that are equal to a specified value.
75
		'eq' => '$eq',
76
		'equals' => '$eq',
77
		'==' => '$eq',
78
		// Matches values that are greater than a specified value.
79
		'gt' => '$gt',
80
		'greater' => '$gt',
81
		'>' => '$gt',
82
		// Matches values that are greater than or equal to a specified value.
83
		'gte' => '$gte',
84
		'greatereq' => '$gte',
85
		'>=' => '$gte',
86
		// Matches values that are less than a specified value.
87
		'lt' => '$lt',
88
		'less' => '$lt',
89
		'<' => '$lt',
90
		// Matches values that are less than or equal to a specified value.
91
		'lte' => '$lte',
92
		'lesseq' => '$lte',
93
		'<=' => '$lte',
94
		// Matches all values that are not equal to a specified value.
95
		'ne' => '$ne',
96
		'noteq' => '$ne',
97
		'!=' => '$ne',
98
		'<>' => '$ne',
99
		// Matches any of the values specified in an array.
100
		'in' => '$in',
101
		// Matches none of the values specified in an array.
102
		'notin' => '$nin',
103
		// Logical
104
		// Joins query clauses with a logical OR returns all documents that match the conditions of either clause.
105
		'or' => '$or',
106
		// Joins query clauses with a logical AND returns all documents that match the conditions of both clauses.
107
		'and' => '$and',
108
		// Inverts the effect of a query expression and returns documents that do not match the query expression.
109
		'not' => '$not',
110
		// Joins query clauses with a logical NOR returns all documents that fail to match both clauses.
111
		'nor' => '$nor',
112
		// Element
113
		// Matches documents that have the specified field.
114
		'exists' => '$exists',
115
		'notexists' => '$exists',
116
		// Selects documents if a field is of the specified type.
117
		'type' => '$type',
118
		// Evaluation
119
		// Performs a modulo operation on the value of a field and selects documents with a specified result.
120
		'mod' => '$mod',
121
		'%' => '$mod',
122
		// Selects documents where values match a specified regular expression.
123
		'regex' => '$regex',
124
		// Performs text search.
125
		'text' => '$text',
126
		// Matches documents that satisfy a JavaScript expression.
127
		'where' => '$where',
128
		// Geospatial
129
		// Selects geometries within a bounding GeoJSON geometry. The `2dsphere` and `2d` indexes support $geoWithin.
130
		'geoWithin' => '$geoWithin',
131
		// Selects geometries that intersect with a GeoJSON geometry. The `2dsphere` index supports $geoIntersects.
132
		'geoIntersects' => '$geoIntersects',
133
		// Returns geospatial objects in proximity to a point. Requires a geospatial index. The `2dsphere` and `2d` indexes support $near.
134
		'near' => '$near',
135
		// Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The `2dsphere` and `2d` indexes support $nearSphere.
136
		'nearSphere' => '$nearSphere',
137
		// Array
138
		// Matches arrays that contain all elements specified in the query.
139
		'all' => '$all',
140
		// Selects documents if element in the array field matches all the specified $elemMatch conditions.
141
		'elemmatch' => '$elemMatch',
142
		// Selects documents if the array field is a specified size.
143
		'size' => '$size',
144
		// Comments
145
		'comment' => '$comment'
146
	];
147
148
	/**
149
	 * Sort Ascending
150
	 */
151
	const SortAsc = SortInterface::SortAsc;
152
153
	/**
154
	 * Sort Descending
155
	 */
156
	const SortDesc = SortInterface::SortDesc;
157
158
	/**
159
	 * Sort Ascending
160
	 * @deprecated since version 4.0.7
161
	 */
162
	const SORT_ASC = SortInterface::SortAsc;
163
164
	/**
165
	 * Sort Descending
166
	 * @deprecated since version 4.0.7
167
	 */
168
	const SORT_DESC = SortInterface::SortDesc;
169
170
	private $_conditions = [];
171
172
	/**
173
	 * Raw conditions array
174
	 * @var mixed[]
175
	 */
176
	private $_rawConds = [];
177
178
	/**
179
	 * Currently used fields list. This is
180
	 * used to allow chained criteria creation.
181
	 *
182
	 * Example:
183
	 *
184
	 * ```
185
	 * $criteria->address->city->street->number = 666
186
	 * ```
187
	 *
188
	 * Will result in conditions:
189
	 *
190
	 * ```
191
	 * [
192
	 *    'address.city.street.number' = 666
193
	 * ]
194
	 * ```
195
	 *
196
	 * @var array
197
	 */
198
	private $_workingFields = [];
199
200
	/**
201
	 * Constructor
202
	 * Example criteria:
203
	 *
204
	 * <pre>
205
	 * $criteria = new Criteria(
206
	 * [
207
	 *    'conditions'=> [
208
	 *        'fieldName1' => ['greater' => 0],
209
	 *        'fieldName2' => ['>=' => 10],
210
	 *        'fieldName3' => ['<' => 10],
211
	 *        'fieldName4' => ['lessEq' => 10],
212
	 *        'fieldName5' => ['notEq' => 10],
213
	 *        'fieldName6' => ['in' => [10, 9]],
214
	 *        'fieldName7' => ['notIn' => [10, 9]],
215
	 *        'fieldName8' => ['all' => [10, 9]],
216
	 *        'fieldName9' => ['size' => 10],
217
	 *        'fieldName10' => ['exists'],
218
	 *        'fieldName11' => ['notExists'],
219
	 *        'fieldName12' => ['mod' => [10, 9]],
220
	 *        'fieldName13' => ['==' => 1]
221
	 *    ],
222
	 *    'select' => [
223
	 *        'fieldName',
224
	 *        'fieldName2'
225
	 *    ],
226
	 *    'limit' => 10,
227
	 *    'offset' => 20,
228
	 *    'sort'=>[
229
	 *        'fieldName1' => Criteria::SortAsc,
230
	 *        'fieldName2' => Criteria::SortDesc,
231
	 *    ]
232
	 * ]
233
	 * );
234
	 * </pre>
235
	 * @param mixed|CriteriaInterface|Conditions $criteria
236
	 * @param AnnotatedInterface|null Model to use for criteria decoration
237
	 * @throws Exception
238
	 */
239 140
	public function __construct($criteria = null, AnnotatedInterface $model = null)
240
	{
241 140
		if (!empty($model))
242
		{
243 24
			$this->setModel($model);
244
		}
245 140
		$this->setCd(new ConditionDecorator($model));
246
247 140
		if (!is_null($criteria) && !is_array($criteria) && !is_object($criteria))
248
		{
249
			$msg = sprintf('Criteria require array or another Criteria object however was provided: %s', $criteria);
250
			throw new UnexpectedValueException($msg);
251
		}
252
253 140
		if (is_array($criteria))
254
		{
255 5
			$available = ['conditions', 'select', 'limit', 'offset', 'sort', 'useCursor'];
256
257 5
			$diff = array_diff_key($criteria, array_flip($available));
258 5
			if (!empty($diff))
259
			{
260
				$params = [
261
					'[' . implode(', ', $available) . ']',
262
					'[' . implode(', ', array_keys($criteria)) . ']'
263
				];
264
				$msg = vsprintf('Allowed criteria keys are: %s, however was provided: %s', $params);
265
				throw new UnexpectedValueException($msg);
266
			}
267
268 5
			if (isset($criteria['conditions']))
269
			{
270 5
				foreach ($criteria['conditions'] as $fieldName => $conditions)
271
				{
272 5
					assert(is_array($conditions), new UnexpectedValueException('Each condition must be array with operator as key and value, ie: ["_id" => ["==" => "123"]]'));
273 5
					foreach ($conditions as $operator => $value)
274
					{
275 5
						$operator = strtolower($operator);
276 5
						if (!isset(self::$operators[$operator]))
277
						{
278
							$params = [
279
								$operator,
280
								$fieldName
281
							];
282
							$msg = vsprintf('Unknown Criteria operator `%s` for `%s`', $params);
283
							throw new UnexpectedValueException($msg);
284
						}
285 5
						$this->addCond($fieldName, $operator, $value);
286
					}
287
				}
288
			}
289
290 5
			if (isset($criteria['select']))
291
			{
292
				$this->select($criteria['select']);
293
			}
294 5
			if (isset($criteria['limit']))
295
			{
296
				$this->limit($criteria['limit']);
297
			}
298 5
			if (isset($criteria['offset']))
299
			{
300
				$this->offset($criteria['offset']);
301
			}
302 5
			if (isset($criteria['sort']))
303
			{
304
				$this->setSort($criteria['sort']);
305
			}
306 5
			if (isset($criteria['useCursor']))
307
			{
308 5
				$this->setUseCursor($criteria['useCursor']);
309
			}
310
		}
311
		// NOTE:
312
		// Scrunitizer: $criteria is of type object<Maslosoft\Mangan\...ria\MergeableInterface>, but the function expects a array|object<Maslosoft\M...aces\CriteriaInterface>.
313
		// But for now it should be this way to easily distinguish from Conditions.
314
		// Future plan: Use CriteriaInterface here, and drop `$criteria instanceof Conditions` if clause. Conditions should implement CriteriaInterface too.
315 138
		elseif ($criteria instanceof MergeableInterface)
316
		{
317
			assert($criteria instanceof CriteriaInterface);
318
			$this->mergeWith($criteria);
319
		}
320 138
		elseif ($criteria instanceof Conditions)
321
		{
322
			$this->setConditions($criteria);
323
		}
324 140
	}
325
326
	/**
327
	 * Merge with other criteria
328
	 * - Field list operators will be merged
329
	 * - Limit and offset will be overridden
330
	 * - Select fields list will be merged
331
	 * - Sort fields list will be merged
332
	 * @param null|array|CriteriaInterface $criteria
333
	 * @return $this
334
	 * @throws Exception
335
	 */
336 135
	public function mergeWith($criteria)
337
	{
338 135
		if (is_array($criteria))
339
		{
340 1
			$criteria = new static($criteria, $this->getModel());
341
		}
342
		elseif (empty($criteria))
343
		{
344 130
			return $this;
345
		}
346
347
		// Set current criteria model if available
348 135
		$model = $this->getModel();
349
350
		// Fall back to merged criteria model
351 135
		if (empty($model))
352
		{
353 127
			$model = $criteria->getModel();
354 127
			if (!empty($model))
355
			{
356 4
				$this->setModel($model);
357
			}
358
		}
359
360
		// Use same model for decorating both criteria,
361
		// current one and merged one
362 135
		if (!empty($model))
363
		{
364 24
			if ($criteria instanceof DecoratableInterface)
365
			{
366 24
				$criteria->decorateWith($model);
367
			}
368
369 24
			if ($criteria instanceof ModelAwareInterface)
370
			{
371 24
				$criteria->setModel($model);
372
			}
373
374 24
			if ($this instanceof DecoratableInterface)
375
			{
376 24
				$this->decorateWith($model);
377
			}
378
379 24
			if ($this instanceof ModelAwareInterface)
380
			{
381 24
				$this->setModel($model);
382
			}
383
		}
384
385
386 135
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getLimit()))
387
		{
388
			$this->setLimit($criteria->getLimit());
389
		}
390 135
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getOffset()))
391
		{
392
			$this->setOffset($criteria->getOffset());
393
		}
394 135
		if ($this instanceof SortableInterface && $criteria instanceof SortableInterface && !empty($criteria->getSort()))
395
		{
396
			$this->setSort($criteria->getSort());
397
		}
398 135
		if ($this instanceof SelectableInterface && $criteria instanceof SelectableInterface && !empty($criteria->getSelect()))
399
		{
400
			$this->select($criteria->getSelect());
401
		}
402
403
404 135
		$this->_conditions = $this->_mergeConditions($this->_conditions, $criteria->getConditions());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_mergeConditions(...teria->getConditions()) of type * is incompatible with the declared type array of property $_conditions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
405 135
		return $this;
406
	}
407
408
	/**
409
	 * Internal method for merging `_conditions` with `getConditions` call result.
410
	 * @param $source
411
	 * @param $conditions
412
	 * @return mixed Merged conditions array
413
	 */
414 140
	private function _mergeConditions($source, $conditions)
415
	{
416 140
		$opTable = array_values(self::$operators);
417 140
		foreach ($conditions as $fieldName => $conds)
418
		{
419
			if (
420 130
				is_array($conds) &&
421 130
				count(array_diff(array_keys($conds), $opTable)) == 0
422
			)
423
			{
424 13
				if (isset($source[$fieldName]) && is_array($source[$fieldName]))
425
				{
426 2
					foreach ($source[$fieldName] as $operator => $value)
427
					{
428 2
						if (!in_array($operator, $opTable))
429
						{
430
							unset($source[$fieldName][$operator]);
431
						}
432
					}
433
				}
434
				else
435
				{
436 13
					$source[$fieldName] = [];
437
				}
438
439 13
				foreach ($conds as $operator => $value)
440
				{
441 13
					$source[$fieldName][$operator] = $value;
442
				}
443
			}
444
			else
445
			{
446 130
				$source[$fieldName] = $conds;
447
			}
448
		}
449 140
		return $source;
450
	}
451
452
	/**
453
	 * By-call-syntax criteria handler
454
	 *
455
	 * @param $fieldName
456
	 * @param mixed $parameters
457
	 * @return $this
458
	 */
459 1
	public function __call($fieldName, $parameters)
460
	{
461 1
		$operatorName = self::$operators['eq'];
462
463
		// Call with operator and value. Set
464
		// first param to be operator.
465 1
		if (array_key_exists(0, $parameters) && array_key_exists(1, $parameters))
466
		{
467 1
			$operatorName = strtolower($parameters[0]);
468
		}
469
470
		// Call without operator, use value only
471 1
		if (array_key_exists(0, $parameters) && !array_key_exists(1, $parameters))
472
		{
473
			$value = $parameters[0];
474
		}
475
476
		// Call with operator and value, use second param as value
477 1
		if (array_key_exists(1, $parameters))
478
		{
479 1
			$value = $parameters[1];
480
		}
481
482
		// ???
483 1
		if (is_numeric($operatorName))
484
		{
485
			$operatorName = strtolower(trim($value));
486
			$value = (strtolower(trim($value)) === 'exists') ? true : false;
487
		}
488
489 1
		if (!in_array($operatorName, array_keys(self::$operators)))
490
		{
491
			throw new UnexpectedValueException("Unknown operator: `$operatorName` on field `$fieldName`");
492
		}
493
494
		/**
495
		 * Support for syntax:
496
		 *
497
		 * ```
498
		 * $criteria->fieldOne->subField('op', 'value')
499
		 * ```
500
		 */
501
502 1
		array_push($this->_workingFields, $fieldName);
503 1
		$fieldName = implode('.', $this->_workingFields);
504 1
		$this->_workingFields = [];
505 1
		switch ($operatorName)
506
		{
507 1
			case 'exists':
508
				$this->addCond($fieldName, $operatorName, true);
509
				break;
510 1
			case 'notexists':
511
				$this->addCond($fieldName, $operatorName, false);
512
				break;
513
			default:
514 1
				$this->addCond($fieldName, $operatorName, $value);
515
		}
516
517 1
		return $this;
518
	}
519
520
	/**
521
	 * This is required for chained criteria creating, ie
522
	 *
523
	 * ```
524
	 * $criteria->fieldOne->fieldTwo = 123;
525
	 * ```
526
	 *
527
	 * @param string $name
528
	 * @return $this
529
	 */
530 2
	public function __get($name)
531
	{
532 2
		array_push($this->_workingFields, $name);
533 2
		return $this;
534
	}
535
536
	/**
537
	 * By-set-syntax handler.
538
	 *
539
	 * This allows adding *equal* conditions by
540
	 * using field.
541
	 *
542
	 * Example:
543
	 *
544
	 * ```
545
	 * $criteria->userId = 1;
546
	 * ```
547
	 *
548
	 * @param string $name
549
	 * @param mixed $value
550
	 */
551 11
	public function __set($name, $value)
552
	{
553 11
		array_push($this->_workingFields, $name);
554 11
		$fieldList = implode('.', $this->_workingFields);
555 11
		$this->_workingFields = [];
556 11
		$this->addCond($fieldList, '==', $value);
557 11
	}
558
559
	/**
560
	 * Return query array
561
	 * @return array Query array
562
	 */
563 140
	public function getConditions()
564
	{
565 140
		$conditions = [];
566 140
		foreach ($this->_rawConds as $c)
567
		{
568 130
			$conditions = $this->_makeCond($c[Conditions::FieldName], $c[Conditions::Operator], $c[Conditions::Value], $conditions);
569
		}
570 140
		return $this->_mergeConditions($this->_conditions, $conditions);
571
	}
572
573
	/**
574
	 * Set conditions
575
	 * @param array|Conditions $conditions
576
	 * @return Criteria
577
	 */
578
	public function setConditions($conditions)
579
	{
580
		if ($conditions instanceof Conditions)
581
		{
582
			$this->_conditions = $conditions->get();
583
			return $this;
584
		}
585
		$this->_conditions = $conditions;
586
		return $this;
587
	}
588
589
	/**
590
	 * Add condition
591
	 * If specified field already has a condition, values will be merged
592
	 * duplicates will be overriden by new values!
593
	 *
594
	 * NOTE: Should NOT be part of interface
595
	 *
596
	 * @param string $fieldName
597
	 * @param string $op operator
598
	 * @param mixed  $value
599
	 * @return $this
600
	 */
601 130
	public function addCond($fieldName, $op, $value)
602
	{
603 130
		$this->_rawConds[] = [
604 130
			Conditions::FieldName => $fieldName,
605 130
			Conditions::Operator => $op,
606 130
			Conditions::Value => $value
607
		];
608 130
		return $this;
609
	}
610
611
	/**
612
	 * Get condition
613
	 * If specified field already has a condition, values will be merged
614
	 * duplicates will be overridden by new values!
615
	 * @see   getConditions
616
	 * @param string $fieldName
617
	 * @param string $op operator
618
	 * @param mixed  $value
619
	 * @param array  $conditions
620
	 * @return array
621
	 */
622 130
	private function _makeCond($fieldName, $op, $value, $conditions = [])
623
	{
624
		// For array values
625
		$arrayOperators = [
626 130
			'or',
627
			'in',
628
			'notin'
629
		];
630 130
		if (in_array($op, $arrayOperators))
631
		{
632
			// Ensure array
633 12
			if (!is_array($value))
634
			{
635 1
				$value = [$value];
636
			}
637
638
			// Decorate each value
639 12
			$values = [];
640 12
			foreach ($value as $val)
641
			{
642 12
				$decorated = $this->getCd()->decorate($fieldName, $val);
643 12
				$fieldName = key($decorated);
644 12
				$values[] = current($decorated);
645
			}
646 12
			$value = $values;
647
		}
648
		else
649
		{
650 130
			$decorated = $this->getCd()->decorate($fieldName, $value);
651 130
			$fieldName = key($decorated);
652 130
			$value = current($decorated);
653
		}
654
655
		// Apply operators
656 130
		$op = self::$operators[$op];
657
658 130
		if ($op == self::$operators['or'])
659
		{
660 1
			if (!isset($conditions[$op]))
661
			{
662 1
				$conditions[$op] = [];
663
			}
664 1
			$conditions[$op][] = [$fieldName => $value];
665
		}
666
		else
667
		{
668 130
			if (!isset($conditions[$fieldName]) && $op != self::$operators['equals'])
669
			{
670 13
				$conditions[$fieldName] = [];
671
			}
672
673 130
			if ($op != self::$operators['equals'])
674
			{
675
				if (
676 13
					!is_array($conditions[$fieldName]) ||
677 13
					count(array_diff(array_keys($conditions[$fieldName]), array_values(self::$operators))) > 0
678
				)
679
				{
680
					$conditions[$fieldName] = [];
681
				}
682 13
				$conditions[$fieldName][$op] = $value;
683
			}
684
			else
685
			{
686 130
				$conditions[$fieldName] = $value;
687
			}
688
		}
689 130
		return $conditions;
690
	}
691
692
}
693