Completed
Push — master ( 6fb532...fb357f )
by Peter
10:26 queued 03:50
created

Criteria::_mergeConditions()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9.0468

Importance

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