Completed
Push — master ( db7816...c3936a )
by Peter
10:14 queued 02:00
created

Criteria::_makeCond()   C

Complexity

Conditions 11
Paths 24

Size

Total Lines 69
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11.0055

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 69
ccs 27
cts 28
cp 0.9643
rs 5.7847
cc 11
eloc 35
nc 24
nop 4
crap 11.0055

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 Maslosoft\Addendum\Interfaces\AnnotatedInterface;
17
use Maslosoft\Mangan\Criteria\ConditionDecorator;
18
use Maslosoft\Mangan\Criteria\Conditions;
19
use Maslosoft\Mangan\Interfaces\Criteria\LimitableInterface;
20
use Maslosoft\Mangan\Interfaces\Criteria\MergeableInterface;
21
use Maslosoft\Mangan\Interfaces\Criteria\SelectableInterface;
22
use Maslosoft\Mangan\Interfaces\Criteria\SortableInterface;
23
use Maslosoft\Mangan\Interfaces\CriteriaInterface;
24
use Maslosoft\Mangan\Interfaces\SortInterface;
25
use Maslosoft\Mangan\Traits\Criteria\CursorAwareTrait;
26
use Maslosoft\Mangan\Traits\Criteria\DecoratableTrait;
27
use Maslosoft\Mangan\Traits\Criteria\LimitableTrait;
28
use Maslosoft\Mangan\Traits\Criteria\SelectableTrait;
29
use Maslosoft\Mangan\Traits\Criteria\SortableTrait;
30
31
/**
32
 * Criteria
33
 *
34
 * This class is a helper for building MongoDB query arrays, it support three syntaxes for adding conditions:
35
 *
36
 * 1. 'equals' syntax:
37
 * 	$criteriaObject->fieldName = $value; // this will produce fieldName == value query
38
 * 2. fieldName call syntax
39
 * 	$criteriaObject->fieldName($operator, $value); // this will produce fieldName <operator> value
40
 * 3. addCond method
41
 * 	$criteriaObject->addCond($fieldName, $operator, $vale); // this will produce fieldName <operator> value
42
 *
43
 * For operators list {@see Criteria::$operators}
44
 *
45
 * @author Ianaré Sévi
46
 * @author Dariusz Górecki <[email protected]>
47
 * @author Invenzzia Group, open-source division of CleverIT company http://www.invenzzia.org
48
 * @copyright 2011 CleverIT http://www.cleverit.com.pl
49
 * @license New BSD license
50
 */
51
class Criteria implements CriteriaInterface
52
{
53
54
	use CursorAwareTrait,
55
	  DecoratableTrait,
56
	  LimitableTrait,
57
	  SelectableTrait,
58
	  SortableTrait;
59
60
	/**
61
	 * @since v1.0
62
	 * @var array $operators supported operators lists
63
	 */
64
	public static $operators = [
65
		// Comparison
66
		// Matches values that are equal to a specified value.
67
		'eq' => '$eq',
68
		'equals' => '$eq',
69
		'==' => '$eq',
70
		// Matches values that are greater than a specified value.
71
		'gt' => '$gt',
72
		'greater' => '$gt',
73
		'>' => '$gt',
74
		// Matches values that are greater than or equal to a specified value.
75
		'gte' => '$gte',
76
		'greatereq' => '$gte',
77
		'>=' => '$gte',
78
		// Matches values that are less than a specified value.
79
		'lt' => '$lt',
80
		'less' => '$lt',
81
		'<' => '$lt',
82
		// Matches values that are less than or equal to a specified value.
83
		'lte' => '$lte',
84
		'lesseq' => '$lte',
85
		'<=' => '$lte',
86
		// Matches all values that are not equal to a specified value.
87
		'ne' => '$ne',
88
		'noteq' => '$ne',
89
		'!=' => '$ne',
90
		'<>' => '$ne',
91
		// Matches any of the values specified in an array.
92
		'in' => '$in',
93
		// Matches none of the values specified in an array.
94
		'notin' => '$nin',
95
		// Logical
96
		// Joins query clauses with a logical OR returns all documents that match the conditions of either clause.
97
		'or' => '$or',
98
		// Joins query clauses with a logical AND returns all documents that match the conditions of both clauses.
99
		'and' => '$and',
100
		// Inverts the effect of a query expression and returns documents that do not match the query expression.
101
		'not' => '$not',
102
		// Joins query clauses with a logical NOR returns all documents that fail to match both clauses.
103
		'nor' => '$nor',
104
		// Element
105
		// Matches documents that have the specified field.
106
		'exists' => '$exists',
107
		'notexists' => '$exists',
108
		// Selects documents if a field is of the specified type.
109
		'type' => '$type',
110
		// Evaluation
111
		// Performs a modulo operation on the value of a field and selects documents with a specified result.
112
		'mod' => '$mod',
113
		'%' => '$mod',
114
		// Selects documents where values match a specified regular expression.
115
		'regex' => '$regex',
116
		// Performs text search.
117
		'text' => '$text',
118
		// Matches documents that satisfy a JavaScript expression.
119
		'where' => '$where',
120
		// Geospatial
121
		// Selects geometries within a bounding GeoJSON geometry. The `2dsphere` and `2d` indexes support $geoWithin.
122
		'geoWithin' => '$geoWithin',
123
		// Selects geometries that intersect with a GeoJSON geometry. The `2dsphere` index supports $geoIntersects.
124
		'geoIntersects' => '$geoIntersects',
125
		// Returns geospatial objects in proximity to a point. Requires a geospatial index. The `2dsphere` and `2d` indexes support $near.
126
		'near' => '$near',
127
		// Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The `2dsphere` and `2d` indexes support $nearSphere.
128
		'nearSphere' => '$nearSphere',
129
		// Array
130
		// Matches arrays that contain all elements specified in the query.
131
		'all' => '$all',
132
		// Selects documents if element in the array field matches all the specified $elemMatch conditions.
133
		'elemmatch' => '$elemMatch',
134
		// Selects documents if the array field is a specified size.
135
		'size' => '$size',
136
		// Comments
137
		'comment' => '$comment'
138
	];
139
140
	/**
141
	 * Sort Ascending
142
	 */
143
	const SortAsc = SortInterface::SortAsc;
144
145
	/**
146
	 * Sort Descending
147
	 */
148
	const SortDesc = SortInterface::SortDesc;
149
150
	/**
151
	 * Sort Ascending
152
	 * @deprecated since version 4.0.7
153
	 */
154
	const SORT_ASC = SortInterface::SortAsc;
155
156
	/**
157
	 * Sort Descending
158
	 * @deprecated since version 4.0.7
159
	 */
160
	const SORT_DESC = SortInterface::SortDesc;
161
162
	private $_conditions = [];
163
164
	/**
165
	 * Raw conditions array
166
	 * @var mixed[]
167
	 */
168
	private $_rawConds = [];
169
	private $_workingFields = [];
170
171
	/**
172
	 * Constructor
173
	 * Example criteria:
174
	 *
175
	 * <PRE>
176
	 * 'criteria' = array(
177
	 * 	'conditions'=>array(
178
	 * 		'fieldName1'=>array('greater' => 0),
179
	 * 		'fieldName2'=>array('>=' => 10),
180
	 * 		'fieldName3'=>array('<' => 10),
181
	 * 		'fieldName4'=>array('lessEq' => 10),
182
	 * 		'fieldName5'=>array('notEq' => 10),
183
	 * 		'fieldName6'=>array('in' => array(10, 9)),
184
	 * 		'fieldName7'=>array('notIn' => array(10, 9)),
185
	 * 		'fieldName8'=>array('all' => array(10, 9)),
186
	 * 		'fieldName9'=>array('size' => 10),
187
	 * 		'fieldName10'=>array('exists'),
188
	 * 		'fieldName11'=>array('notExists'),
189
	 * 		'fieldName12'=>array('mod' => array(10, 9)),
190
	 * 		'fieldName13'=>array('==' => 1)
191
	 * 	),
192
	 * 	'select'=>array('fieldName', 'fieldName2'),
193
	 * 	'limit'=>10,
194
	 *  'offset'=>20,
195
	 *  'sort'=>array('fieldName1'=>Criteria::SortAsc, 'fieldName2'=>Criteria::SortDesc),
196
	 * );
197
	 * </PRE>
198
	 * @param mixed|CriteriaInterface|Conditions $criteria
199
	 * @param AnnotatedInterface|null Model to use for criteria decoration
200
	 * @since v1.0
201
	 */
202 88
	public function __construct($criteria = null, AnnotatedInterface $model = null)
203
	{
204 88
		$this->setCd(new ConditionDecorator($model));
205 88
		if (is_array($criteria))
206
		{
207 1
			if (isset($criteria['conditions']))
208
				foreach ($criteria['conditions'] as $fieldName => $conditions)
209
				{
210
					$fieldNameArray = explode('.', $fieldName);
211
					if (count($fieldNameArray) === 1)
212
					{
213
						$fieldName = array_shift($fieldNameArray);
214
					}
215
					else
216
					{
217
						$fieldName = array_pop($fieldNameArray);
218
					}
219
220
					$this->_workingFields = $fieldNameArray;
221
					foreach ($conditions as $operator => $value)
222
					{
223
						$operator = strtolower($operator);
224
						$this->addCond($fieldName, $operator, $value);
225
					}
226
				}
227
228 1
			if (isset($criteria['select']))
229
			{
230
				$this->select($criteria['select']);
231
			}
232 1
			if (isset($criteria['limit']))
233
			{
234
				$this->limit($criteria['limit']);
235
			}
236 1
			if (isset($criteria['offset']))
237
			{
238
				$this->offset($criteria['offset']);
239
			}
240 1
			if (isset($criteria['sort']))
241
			{
242
				$this->setSort($criteria['sort']);
243
			}
244 1
			if (isset($criteria['useCursor']))
245
			{
246 1
				$this->setUseCursor($criteria['useCursor']);
247
			}
248
		}
249
		// NOTE:
250
		//Scrunitizer: $criteria is of type object<Maslosoft\Mangan\...ria\MergeableInterface>, but the function expects a array|object<Maslosoft\M...aces\CriteriaInterface>.
251
		// But for now it should be this way to easyli distinguish from Conditions.
252
		// Future plan: Use CriteriaInterface here, and drop `$criteria instanceof Conditions` if clause. Conditions should implement CriteriaInterface too.
253
		elseif ($criteria instanceof MergeableInterface)
254
		{
255
			assert($criteria instanceof CriteriaInterface);
256
			$this->mergeWith($criteria);
257
		}
258
		elseif ($criteria instanceof Conditions)
259
		{
260
			$this->setConditions($criteria);
261
		}
262 88
	}
263
264
	/**
265
	 * Merge with other criteria
266
	 * - Field list operators will be merged
267
	 * - Limit and offet will be overriden
268
	 * - Select fields list will be merged
269
	 * - Sort fields list will be merged
270
	 * @param array|CriteriaInterface $criteria
271
	 * @return CriteriaInterface
272
	 * @since v1.0
273
	 */
274 81
	public function mergeWith($criteria)
275
	{
276 81
		if (is_array($criteria))
277
		{
278
			$criteria = new static($criteria);
279
		}
280
		elseif (empty($criteria))
281
		{
282 81
			return $this;
283
		}
284
285 81
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getLimit()))
286
		{
287
			$this->setLimit($criteria->getLimit());
288
		}
289 81
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getOffset()))
290
		{
291
			$this->setOffset($criteria->getOffset());
292
		}
293 81
		if ($this instanceof SortableInterface && $criteria instanceof SortableInterface && !empty($criteria->getSort()))
294
		{
295
			$this->setSort($criteria->getSort());
296
		}
297 81
		if ($this instanceof SelectableInterface && $criteria instanceof SelectableInterface && !empty($criteria->getSelect()))
298
		{
299
			$this->select($criteria->getSelect());
300
		}
301
302
303
304 81
		$this->_conditions = $this->_mergeConditions($this->_conditions, $criteria->getConditions());
305
306 81
		return $this;
307
	}
308
309 88
	private function _mergeConditions($source, $conditions)
310
	{
311 88
		$opTable = array_values(self::$operators);
312 88
		foreach ($conditions as $fieldName => $conds)
313
		{
314
			if (
315 83
					is_array($conds) &&
316 83
					count(array_diff(array_keys($conds), $opTable)) == 0
317
			)
318
			{
319 11
				if (isset($source[$fieldName]) && is_array($source[$fieldName]))
320
				{
321 1
					foreach ($source[$fieldName] as $operator => $value)
322
					{
323 1
						if (!in_array($operator, $opTable))
324
						{
325 1
							unset($source[$fieldName][$operator]);
326
						}
327
					}
328
				}
329
				else
330
				{
331 11
					$source[$fieldName] = [];
332
				}
333
334 11
				foreach ($conds as $operator => $value)
335
				{
336 11
					$source[$fieldName][$operator] = $value;
337
				}
338
			}
339
			else
340
			{
341 83
				$source[$fieldName] = $conds;
342
			}
343
		}
344 88
		return $source;
345
	}
346
347
	/**
348
	 * If we have operator add it otherwise call parent implementation
349
	 * @since v1.0
350
	 */
351 1
	public function __call($fieldName, $parameters)
352
	{
353 1
		if (isset($parameters[0]))
354
		{
355 1
			$operatorName = strtolower($parameters[0]);
356
		}
357 1
		if (array_key_exists(1, $parameters))
358
		{
359 1
			$value = $parameters[1];
360
		}
361 1
		if (is_numeric($operatorName))
362
		{
363
			$operatorName = strtolower(trim($value));
364
			$value = (strtolower(trim($value)) === 'exists') ? true : false;
365
		}
366
367 1
		if (in_array($operatorName, array_keys(self::$operators)))
368
		{
369 1
			array_push($this->_workingFields, $fieldName);
370 1
			$fieldName = implode('.', $this->_workingFields);
371 1
			$this->_workingFields = [];
372
			switch ($operatorName)
373
			{
374 1
				case 'exists':
375
					$this->addCond($fieldName, $operatorName, true);
376
					break;
377 1
				case 'notexists':
378
					$this->addCond($fieldName, $operatorName, false);
379
					break;
380
				default:
381 1
					$this->addCond($fieldName, $operatorName, $value);
382
			}
383 1
			return $this;
384
		}
385
	}
386
387
	/**
388
	 * @since v1.0.2
389
	 */
390
	public function __get($name)
391
	{
392
		array_push($this->_workingFields, $name);
393
		return $this;
394
	}
395
396
	/**
397
	 * @since v1.0.2
398
	 */
399 12
	public function __set($name, $value)
400
	{
401 12
		array_push($this->_workingFields, $name);
402 12
		$fieldList = implode('.', $this->_workingFields);
403 12
		$this->_workingFields = [];
404 12
		$this->addCond($fieldList, '==', $value);
405 12
	}
406
407
	/**
408
	 * Return query array
409
	 * @return array query array
410
	 * @since v1.0
411
	 */
412 88
	public function getConditions()
413
	{
414 88
		$conditions = [];
415 88
		foreach ($this->_rawConds as $c)
416
		{
417 83
			$conditions = $this->_makeCond($c[Conditions::FieldName], $c[Conditions::Operator], $c[Conditions::Value], $conditions);
418
		}
419 88
		return $this->_mergeConditions($this->_conditions, $conditions);
420
	}
421
422
	/**
423
	 * Set conditions
424
	 * @param array|Conditions $conditions
425
	 * @return Criteria
426
	 */
427
	public function setConditions($conditions)
428
	{
429
		if ($conditions instanceof Conditions)
430
		{
431
			$this->_conditions = $conditions->get();
432
			return $this;
433
		}
434
		$this->_conditions = $conditions;
435
		return $this;
436
	}
437
438
	/**
439
	 * Add condition
440
	 * If specified field already has a condition, values will be merged
441
	 * duplicates will be overriden by new values!
442
	 *
443
	 * NOTE: Should NOT be part of interface
444
	 *
445
	 * @param string $fieldName
446
	 * @param string $op operator
447
	 * @param mixed $value
448
	 * @since v1.0
449
	 */
450 83
	public function addCond($fieldName, $op, $value)
451
	{
452 83
		$this->_rawConds[] = [
453 83
			Conditions::FieldName => $fieldName,
454 83
			Conditions::Operator => $op,
455 83
			Conditions::Value => $value
456
		];
457 83
		return $this;
458
	}
459
460
	/**
461
	 * Get condition
462
	 * If specified field already has a condition, values will be merged
463
	 * duplicates will be overridden by new values!
464
	 * @see getConditions
465
	 * @param string $fieldName
466
	 * @param string $op operator
467
	 * @param mixed $value
468
	 * @since v1.0
469
	 */
470 83
	private function _makeCond($fieldName, $op, $value, $conditions = [])
471
	{
472
		// For array values
473
		$arrayOperators = [
474 83
			'or',
475
			'in',
476
			'notin'
477
		];
478 83
		if (in_array($op, $arrayOperators))
479
		{
480
			// Ensure array
481 9
			if (!is_array($value))
482
			{
483 1
				$value = [$value];
484
			}
485
486
			// Decorate each value
487 9
			$values = [];
488 9
			foreach ($value as $val)
489
			{
490 9
				$decorated = $this->getCd()->decorate($fieldName, $val);
491 9
				$fieldName = key($decorated);
492 9
				$values[] = current($decorated);
493
			}
494 9
			$value = $values;
495
		}
496
		else
497
		{
498 83
			$decorated = $this->getCd()->decorate($fieldName, $value);
499 83
			$fieldName = key($decorated);
500 83
			$value = current($decorated);
501
		}
502
503
		// Apply operators
504 83
		$op = self::$operators[$op];
505
506 83
		if ($op == self::$operators['or'])
507
		{
508 1
			if (!isset($conditions[$op]))
509
			{
510 1
				$conditions[$op] = [];
511
			}
512 1
			$conditions[$op][] = [$fieldName => $value];
513
		}
514
		else
515
		{
516 83
			if (!isset($conditions[$fieldName]) && $op != self::$operators['equals'])
517
			{
518 11
				$conditions[$fieldName] = [];
519
			}
520
521 83
			if ($op != self::$operators['equals'])
522
			{
523
				if (
524 11
						!is_array($conditions[$fieldName]) ||
525 11
						count(array_diff(array_keys($conditions[$fieldName]), array_values(self::$operators))) > 0
526
				)
527
				{
528
					$conditions[$fieldName] = [];
529
				}
530 11
				$conditions[$fieldName][$op] = $value;
531
			}
532
			else
533
			{
534 83
				$conditions[$fieldName] = $value;
535
			}
536
		}
537 83
		return $conditions;
538
	}
539
540
}
541