Completed
Push — master ( 51e00b...69ef22 )
by Peter
05:35
created

Criteria::__call()   C

Complexity

Conditions 8
Paths 48

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.3696

Importance

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