Completed
Push — master ( 4182a1...6a0ca3 )
by Peter
05:51
created

Criteria::__set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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