Completed
Push — master ( 308150...7a3b42 )
by Peter
08:01
created

Criteria   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 508
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 83.64%

Importance

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

12 Methods

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

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 http://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
26
/**
27
 * Criteria
28
 *
29
 * This class is a helper for building MongoDB query arrays, it support three syntaxes for adding conditions:
30
 *
31
 * 1. 'equals' syntax:
32
 * 	$criteriaObject->fieldName = $value; // this will produce fieldName == value query
33
 * 2. fieldName call syntax
34
 * 	$criteriaObject->fieldName($operator, $value); // this will produce fieldName <operator> value
35
 * 3. addCond method
36
 * 	$criteriaObject->addCond($fieldName, $operator, $vale); // this will produce fieldName <operator> value
37
 *
38
 * For operators list {@see Criteria::$operators}
39
 *
40
 * @author Ianaré Sévi
41
 * @author Dariusz Górecki <[email protected]>
42
 * @author Invenzzia Group, open-source division of CleverIT company http://www.invenzzia.org
43
 * @copyright 2011 CleverIT http://www.cleverit.com.pl
44
 * @license New BSD license
45
 */
46
class Criteria implements CriteriaInterface
47
{
48
49
	use Traits\Criteria\CursorAwareTrait,
50
	  Traits\Criteria\DecoratableTrait,
51
	  Traits\Criteria\LimitableTrait,
52
	  Traits\Criteria\SelectableTrait,
53
	  Traits\Criteria\SortableTrait;
54
55
	/**
56
	 * @since v1.0
57
	 * @var array $operators supported operators lists
58
	 */
59
	public static $operators = [
60
61
		// Comparison
62
		// Matches values that are equal to a specified value.
63
		'eq' => '$eq',
64
		'equals' => '$eq',
65
		'==' => '$eq',
66
		// Matches values that are greater than a specified value.
67
		'gt' => '$gt',
68
		'greater' => '$gt',
69
		'>' => '$gt',
70
		// Matches values that are greater than or equal to a specified value.
71
		'gte' => '$gte',
72
		'greatereq' => '$gte',
73
		'>=' => '$gte',
74
		// Matches values that are less than a specified value.
75
		'lt' => '$lt',
76
		'less' => '$lt',
77
		'<' => '$lt',
78
		// Matches values that are less than or equal to a specified value.
79
		'lte' => '$lte',
80
		'lesseq' => '$lte',
81
		'<=' => '$lte',
82
		// Matches all values that are not equal to a specified value.
83
		'ne' => '$ne',
84
		'noteq' => '$ne',
85
		'!=' => '$ne',
86
		'<>' => '$ne',
87
		// Matches any of the values specified in an array.
88
		'in' => '$in',
89
		// Matches none of the values specified in an array.
90
		'notin' => '$nin',
91
		// Logical
92
		// Joins query clauses with a logical OR returns all documents that match the conditions of either clause.
93
		'or' => '$or',
94
		// Joins query clauses with a logical AND returns all documents that match the conditions of both clauses.
95
		'and' => '$and',
96
		// Inverts the effect of a query expression and returns documents that do not match the query expression.
97
		'not' => '$not',
98
		// Joins query clauses with a logical NOR returns all documents that fail to match both clauses.
99
		'nor' => '$nor',
100
		// Element
101
		// Matches documents that have the specified field.
102
		'exists' => '$exists',
103
		'notexists' => '$exists',
104
		// Selects documents if a field is of the specified type.
105
		'type' => '$type',
106
		// Evaluation
107
		// Performs a modulo operation on the value of a field and selects documents with a specified result.
108
		'mod' => '$mod',
109
		'%' => '$mod',
110
		// Selects documents where values match a specified regular expression.
111
		'regex' => '$regex',
112
		// Performs text search.
113
		'text' => '$text',
114
		// Matches documents that satisfy a JavaScript expression.
115
		'where' => '$where',
116
		// Geospatial
117
		// Selects geometries within a bounding GeoJSON geometry. The `2dsphere` and `2d` indexes support $geoWithin.
118
		'geoWithin' => '$geoWithin',
119
		// Selects geometries that intersect with a GeoJSON geometry. The `2dsphere` index supports $geoIntersects.
120
		'geoIntersects' => '$geoIntersects',
121
		// Returns geospatial objects in proximity to a point. Requires a geospatial index. The `2dsphere` and `2d` indexes support $near.
122
		'near' => '$near',
123
		// Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The `2dsphere` and `2d` indexes support $nearSphere.
124
		'nearSphere' => '$nearSphere',
125
		// Array
126
		// Matches arrays that contain all elements specified in the query.
127
		'all' => '$all',
128
		// Selects documents if element in the array field matches all the specified $elemMatch conditions.
129
		'elemmatch' => '$elemMatch',
130
		// Selects documents if the array field is a specified size.
131
		'size' => '$size',
132
		// Comments
133
		'comment' => '$comment'
134
	];
135
136
	/**
137
	 * Sort Ascending
138
	 */
139
	const SortAsc = SortInterface::SortAsc;
140
141
	/**
142
	 * Sort Descending
143
	 */
144
	const SortDesc = SortInterface::SortDesc;
145
146
	/**
147
	 * Sort Ascending
148
	 * @deprecated since version 4.0.7
149
	 */
150
	const SORT_ASC = SortInterface::SortAsc;
151
152
	/**
153
	 * Sort Descending
154
	 * @deprecated since version 4.0.7
155
	 */
156
	const SORT_DESC = SortInterface::SortDesc;
157
158
	private $_conditions = [];
159
160
	/**
161
	 * Raw conditions array
162
	 * @var mixed[]
163
	 */
164
	private $_rawConds = [];
165
	private $_workingFields = [];
166
167
	/**
168
	 * Constructor
169
	 * Example criteria:
170
	 *
171
	 * <PRE>
172
	 * 'criteria' = array(
173
	 * 	'conditions'=>array(
174
	 * 		'fieldName1'=>array('greater' => 0),
175
	 * 		'fieldName2'=>array('>=' => 10),
176
	 * 		'fieldName3'=>array('<' => 10),
177
	 * 		'fieldName4'=>array('lessEq' => 10),
178
	 * 		'fieldName5'=>array('notEq' => 10),
179
	 * 		'fieldName6'=>array('in' => array(10, 9)),
180
	 * 		'fieldName7'=>array('notIn' => array(10, 9)),
181
	 * 		'fieldName8'=>array('all' => array(10, 9)),
182
	 * 		'fieldName9'=>array('size' => 10),
183
	 * 		'fieldName10'=>array('exists'),
184
	 * 		'fieldName11'=>array('notExists'),
185
	 * 		'fieldName12'=>array('mod' => array(10, 9)),
186
	 * 		'fieldName13'=>array('==' => 1)
187
	 * 	),
188
	 * 	'select'=>array('fieldName', 'fieldName2'),
189
	 * 	'limit'=>10,
190
	 *  'offset'=>20,
191
	 *  'sort'=>array('fieldName1'=>Criteria::SortAsc, 'fieldName2'=>Criteria::SortDesc),
192
	 * );
193
	 * </PRE>
194
	 * @param mixed|CriteriaInterface|Conditions $criteria
195
	 * @param AnnotatedInterface|null Model to use for criteria decoration
196
	 * @since v1.0
197
	 */
198 86
	public function __construct($criteria = null, AnnotatedInterface $model = null)
199
	{
200 86
		$this->setCd(new ConditionDecorator($model));
201 86
		if (is_array($criteria))
202
		{
203 1
			if (isset($criteria['conditions']))
204
				foreach ($criteria['conditions'] as $fieldName => $conditions)
205
				{
206
					$fieldNameArray = explode('.', $fieldName);
207
					if (count($fieldNameArray) === 1)
208
					{
209
						$fieldName = array_shift($fieldNameArray);
210
					}
211
					else
212
					{
213
						$fieldName = array_pop($fieldNameArray);
214
					}
215
216
					foreach ($conditions as $operator => $value)
217
					{
218
						$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...
219
						$operator = strtolower($operator);
220
						$this->addCond($fieldName, $operator, $value);
221
					}
222
				}
223
224 1
			if (isset($criteria['select']))
225
			{
226
				$this->select($criteria['select']);
227
			}
228 1
			if (isset($criteria['limit']))
229
			{
230
				$this->limit($criteria['limit']);
231
			}
232 1
			if (isset($criteria['offset']))
233
			{
234
				$this->offset($criteria['offset']);
235
			}
236 1
			if (isset($criteria['sort']))
237
			{
238
				$this->setSort($criteria['sort']);
239
			}
240 1
			if (isset($criteria['useCursor']))
241
			{
242 1
				$this->setUseCursor($criteria['useCursor']);
243
			}
244
		}
245
		// NOTE:
246
		//Scrunitizer: $criteria is of type object<Maslosoft\Mangan\...ria\MergeableInterface>, but the function expects a array|object<Maslosoft\M...aces\CriteriaInterface>.
247
		// But for now it should be this way to easyli distinguish from Conditions.
248
		// Future plan: Use CriteriaInterface here, and drop `$criteria instanceof Conditions` if clause. Conditions should implement CriteriaInterface too.
249
		elseif ($criteria instanceof MergeableInterface)
250
		{
251
			$this->mergeWith($criteria);
0 ignored issues
show
Documentation introduced by
$criteria is of type object<Maslosoft\Mangan\...ria\MergeableInterface>, but the function expects a array|object<Maslosoft\M...aces\CriteriaInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
252
		}
253
		elseif ($criteria instanceof Conditions)
254
		{
255
			$this->setConditions($criteria);
256
		}
257 86
	}
258
259
	/**
260
	 * Merge with other criteria
261
	 * - Field list operators will be merged
262
	 * - Limit and offet will be overriden
263
	 * - Select fields list will be merged
264
	 * - Sort fields list will be merged
265
	 * @param array|CriteriaInterface $criteria
266
	 * @return CriteriaInterface
267
	 * @since v1.0
268
	 */
269 80
	public function mergeWith($criteria)
270
	{
271 80
		if (is_array($criteria))
272
		{
273
			$criteria = new static($criteria);
274
		}
275
		elseif (empty($criteria))
276
		{
277 80
			return $this;
278
		}
279
280 40
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getLimit()))
281
		{
282
			$this->setLimit($criteria->getLimit());
283
		}
284 40
		if ($this instanceof LimitableInterface && $criteria instanceof LimitableInterface && !empty($criteria->getOffset()))
285
		{
286
			$this->setOffset($criteria->getOffset());
287
		}
288 40
		if ($this instanceof SortableInterface && $criteria instanceof SortableInterface && !empty($criteria->getSort()))
289
		{
290
			$this->setSort($criteria->getSort());
291
		}
292 40
		if ($this instanceof SelectableInterface && $criteria instanceof SelectableInterface && !empty($criteria->getSelect()))
293
		{
294
			$this->select($criteria->getSelect());
295
		}
296
297
298
299 40
		$this->_conditions = $this->_mergeConditions($this->_conditions, $criteria->getConditions());
300
301 40
		return $this;
302
	}
303
304 86
	private function _mergeConditions($source, $conditions)
305
	{
306 86
		$opTable = array_values(self::$operators);
307 86
		foreach ($conditions as $fieldName => $conds)
308
		{
309
			if (
310 82
					is_array($conds) &&
311 82
					count(array_diff(array_keys($conds), $opTable)) == 0
312
			)
313
			{
314 11
				if (isset($source[$fieldName]) && is_array($source[$fieldName]))
315
				{
316 1
					foreach ($source[$fieldName] as $operator => $value)
317
					{
318 1
						if (!in_array($operator, $opTable))
319
						{
320 1
							unset($source[$fieldName][$operator]);
321
						}
322
					}
323
				}
324
				else
325
				{
326 11
					$source[$fieldName] = [];
327
				}
328
329 11
				foreach ($conds as $operator => $value)
330
				{
331 11
					$source[$fieldName][$operator] = $value;
332
				}
333
			}
334
			else
335
			{
336 82
				$source[$fieldName] = $conds;
337
			}
338
		}
339 86
		return $source;
340
	}
341
342
	/**
343
	 * If we have operator add it otherwise call parent implementation
344
	 * @since v1.0
345
	 */
346 1
	public function __call($fieldName, $parameters)
347
	{
348 1
		if (isset($parameters[0]))
349
		{
350 1
			$operatorName = strtolower($parameters[0]);
351
		}
352 1
		if (array_key_exists(1, $parameters))
353
		{
354 1
			$value = $parameters[1];
355
		}
356 1
		if (is_numeric($operatorName))
357
		{
358
			$operatorName = strtolower(trim($value));
359
			$value = (strtolower(trim($value)) === 'exists') ? true : false;
360
		}
361
362 1
		if (in_array($operatorName, array_keys(self::$operators)))
363
		{
364 1
			array_push($this->_workingFields, $fieldName);
365 1
			$fieldName = implode('.', $this->_workingFields);
366 1
			$this->_workingFields = [];
367
			switch ($operatorName)
368
			{
369 1
				case 'exists':
370
					$this->addCond($fieldName, $operatorName, true);
371
					break;
372 1
				case 'notexists':
373
					$this->addCond($fieldName, $operatorName, false);
374
					break;
375
				default:
376 1
					$this->addCond($fieldName, $operatorName, $value);
377
			}
378 1
			return $this;
379
		}
380
	}
381
382
	/**
383
	 * @since v1.0.2
384
	 */
385
	public function __get($name)
386
	{
387
		array_push($this->_workingFields, $name);
388
		return $this;
389
	}
390
391
	/**
392
	 * @since v1.0.2
393
	 */
394 11
	public function __set($name, $value)
395
	{
396 11
		array_push($this->_workingFields, $name);
397 11
		$fieldList = implode('.', $this->_workingFields);
398 11
		$this->_workingFields = [];
399 11
		$this->addCond($fieldList, '==', $value);
400 11
	}
401
402
	/**
403
	 * Return query array
404
	 * @return array query array
405
	 * @since v1.0
406
	 */
407 86
	public function getConditions()
408
	{
409 86
		$conditions = [];
410 86
		foreach ($this->_rawConds as $c)
411
		{
412 82
			$conditions = $this->_makeCond($c[Conditions::FieldName], $c[Conditions::Operator], $c[Conditions::Value], $conditions);
413
		}
414 86
		return $this->_mergeConditions($this->_conditions, $conditions);
415
	}
416
417
	/**
418
	 * Set conditions
419
	 * @param array|Conditions $conditions
420
	 * @return Criteria
421
	 */
422
	public function setConditions($conditions)
423
	{
424
		if ($conditions instanceof Conditions)
425
		{
426
			$this->_conditions = $conditions->get();
427
			return $this;
428
		}
429
		$this->_conditions = $conditions;
430
		return $this;
431
	}
432
433
	/**
434
	 * Add condition
435
	 * If specified field already has a condition, values will be merged
436
	 * duplicates will be overriden by new values!
437
	 *
438
	 * NOTE: Should NOT be part of interface
439
	 *
440
	 * @param string $fieldName
441
	 * @param string $op operator
442
	 * @param mixed $value
443
	 * @since v1.0
444
	 */
445 82
	public function addCond($fieldName, $op, $value)
446
	{
447 82
		$this->_rawConds[] = [
448 82
			Conditions::FieldName => $fieldName,
449 82
			Conditions::Operator => $op,
450 82
			Conditions::Value => $value
451
		];
452 82
		return $this;
453
	}
454
455
	/**
456
	 * @since v1.3.1
457
	 * @deprecated since version number
458
	 */
459
	protected function getWorkingFields()
460
	{
461
		return $this->_workingFields;
462
	}
463
464
	/**
465
	 * @since v1.3.1
466
	 * @deprecated since version number
467
	 */
468
	protected function setWorkingFields(array $select)
469
	{
470
		$this->_workingFields = $select;
471
	}
472
473
	/**
474
	 * Get condition
475
	 * If specified field already has a condition, values will be merged
476
	 * duplicates will be overriden by new values!
477
	 * @see getConditions
478
	 * @param string $fieldName
479
	 * @param string $op operator
480
	 * @param mixed $value
481
	 * @since v1.0
482
	 */
483 82
	private function _makeCond($fieldName, $op, $value, $conditions = [])
484
	{
485
		// For array values
486
		$arrayOperators = [
487 82
			'or',
488
			'in',
489
			'notin'
490
		];
491 82
		if (in_array($op, $arrayOperators))
492
		{
493
			// Ensure array
494 9
			if (!is_array($value))
495
			{
496 1
				$value = [$value];
497
			}
498
499
			// Decorate each value
500 9
			$values = [];
501 9
			foreach ($value as $val)
502
			{
503 9
				$decorated = $this->getCd()->decorate($fieldName, $val);
504 9
				$fieldName = key($decorated);
505 9
				$values[] = current($decorated);
506
			}
507 9
			$value = $values;
508
		}
509
		else
510
		{
511 82
			$decorated = $this->getCd()->decorate($fieldName, $value);
512 82
			$fieldName = key($decorated);
513 82
			$value = current($decorated);
514
		}
515
516
		// Apply operators
517 82
		$op = self::$operators[$op];
518
519 82
		if ($op == self::$operators['or'])
520
		{
521 1
			if (!isset($conditions[$op]))
522
			{
523 1
				$conditions[$op] = [];
524
			}
525 1
			$conditions[$op][] = [$fieldName => $value];
526
		}
527
		else
528
		{
529 82
			if (!isset($conditions[$fieldName]) && $op != self::$operators['equals'])
530
			{
531 11
				$conditions[$fieldName] = [];
532
			}
533
534 82
			if ($op != self::$operators['equals'])
535
			{
536
				if (
537 11
						!is_array($conditions[$fieldName]) ||
538 11
						count(array_diff(array_keys($conditions[$fieldName]), array_values(self::$operators))) > 0
539
				)
540
				{
541
					$conditions[$fieldName] = [];
542
				}
543 11
				$conditions[$fieldName][$op] = $value;
544
			}
545
			else
546
			{
547 82
				$conditions[$fieldName] = $value;
548
			}
549
		}
550 82
		return $conditions;
551
	}
552
553
}
554