Completed
Push — master ( b4b35b...273652 )
by Peter
05:39
created

Criteria::__construct()   C

Complexity

Conditions 14
Paths 134

Size

Total Lines 69
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 55.0163

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 69
ccs 13
cts 32
cp 0.4063
rs 5.2268
cc 14
eloc 32
nc 134
nop 2
crap 55.0163

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