Completed
Push — master ( 3089d5...0e37ee )
by Peter
08:44 queued 02:44
created

Criteria   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 508
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 82.88%

Importance

Changes 0
Metric Value
wmc 65
lcom 1
cbo 9
dl 0
loc 508
ccs 92
cts 111
cp 0.8288
rs 5.7894
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 61 13
C mergeWith() 0 34 15
D _mergeConditions() 0 37 9
C __call() 0 35 8
A __get() 0 5 1
A __set() 0 7 1
A getConditions() 0 9 2
A setConditions() 0 10 2
A addCond() 0 9 1
A getWorkingFields() 0 4 1
A setWorkingFields() 0 4 1
C _makeCond() 0 69 11

How to fix   Complexity   

Complex Class

Complex classes like Criteria often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Criteria, and based on these observations, apply Extract Interface, too.

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
					foreach ($conditions as $operator => $value)
221
					{
222
						$this->setWorkingFields($fieldNameArray);
0 ignored issues
show
Deprecated Code introduced by
The method Maslosoft\Mangan\Criteria::setWorkingFields() has been deprecated with message: since version number

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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
	 * @since v1.3.1
462
	 * @deprecated since version number
463
	 */
464
	protected function getWorkingFields()
465
	{
466
		return $this->_workingFields;
467
	}
468
469
	/**
470
	 * @since v1.3.1
471
	 * @deprecated since version number
472
	 */
473
	protected function setWorkingFields(array $select)
474
	{
475
		$this->_workingFields = $select;
476
	}
477
478
	/**
479
	 * Get condition
480
	 * If specified field already has a condition, values will be merged
481
	 * duplicates will be overriden by new values!
482
	 * @see getConditions
483
	 * @param string $fieldName
484
	 * @param string $op operator
485
	 * @param mixed $value
486
	 * @since v1.0
487
	 */
488 83
	private function _makeCond($fieldName, $op, $value, $conditions = [])
489
	{
490
		// For array values
491
		$arrayOperators = [
492 83
			'or',
493
			'in',
494
			'notin'
495
		];
496 83
		if (in_array($op, $arrayOperators))
497
		{
498
			// Ensure array
499 9
			if (!is_array($value))
500
			{
501 1
				$value = [$value];
502
			}
503
504
			// Decorate each value
505 9
			$values = [];
506 9
			foreach ($value as $val)
507
			{
508 9
				$decorated = $this->getCd()->decorate($fieldName, $val);
509 9
				$fieldName = key($decorated);
510 9
				$values[] = current($decorated);
511
			}
512 9
			$value = $values;
513
		}
514
		else
515
		{
516 83
			$decorated = $this->getCd()->decorate($fieldName, $value);
517 83
			$fieldName = key($decorated);
518 83
			$value = current($decorated);
519
		}
520
521
		// Apply operators
522 83
		$op = self::$operators[$op];
523
524 83
		if ($op == self::$operators['or'])
525
		{
526 1
			if (!isset($conditions[$op]))
527
			{
528 1
				$conditions[$op] = [];
529
			}
530 1
			$conditions[$op][] = [$fieldName => $value];
531
		}
532
		else
533
		{
534 83
			if (!isset($conditions[$fieldName]) && $op != self::$operators['equals'])
535
			{
536 11
				$conditions[$fieldName] = [];
537
			}
538
539 83
			if ($op != self::$operators['equals'])
540
			{
541
				if (
542 11
						!is_array($conditions[$fieldName]) ||
543 11
						count(array_diff(array_keys($conditions[$fieldName]), array_values(self::$operators))) > 0
544
				)
545
				{
546
					$conditions[$fieldName] = [];
547
				}
548 11
				$conditions[$fieldName][$op] = $value;
549
			}
550
			else
551
			{
552 83
				$conditions[$fieldName] = $value;
553
			}
554
		}
555 83
		return $conditions;
556
	}
557
558
}
559