Completed
Push — master ( fb357f...51b9c7 )
by Peter
06:58
created

Finder::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 3
crap 3
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\Events\Event;
18
use Maslosoft\Mangan\Exceptions\ManganException;
19
use Maslosoft\Mangan\Helpers\PkManager;
20
use Maslosoft\Mangan\Interfaces\CriteriaInterface;
21
use Maslosoft\Mangan\Interfaces\EntityManagerInterface;
22
use Maslosoft\Mangan\Interfaces\FinderInterface;
23
use Maslosoft\Mangan\Interfaces\ScenariosInterface;
24
use Maslosoft\Mangan\Meta\ManganMeta;
25
use Maslosoft\Mangan\Transformers\RawArray;
26
use MongoCursor;
27
28
/**
29
 * Finder
30
 *
31
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
32
 */
33
class Finder implements FinderInterface
34
{
35
36
	/**
37
	 * Model
38
	 * @var AnnotatedInterface
39
	 */
40
	public $model = null;
41
42
	/**
43
	 * Mangan instance
44
	 * @var Mangan
45
	 */
46
	private $mn = null;
47
48
	/**
49
	 * Entity manager instance
50
	 * @var EntityManagerInterface
51
	 */
52
	private $em = null;
53
54
	/**
55
	 * Scope manager instance
56
	 * @var ScopeManager
57
	 */
58
	private $sm = null;
59
60
	/**
61
	 * Finder criteria
62
	 * @var Criteria
63
	 */
64
	private $_criteria = null;
65
66
	/**
67
	 * Whenever to use corsors
68
	 * @var bool
69
	 */
70
	private $_useCursor = false;
71
72
	/**
73
	 * Constructor
74
	 *
75
	 * @param object $model Model instance
76
	 * @param EntityManagerInterface $em
77
	 * @param Mangan $mangan
78
	 */
79 67
	public function __construct($model, $em = null, $mangan = null)
80
	{
81 67
		$this->model = $model;
82 67
		$this->sm = new ScopeManager($model);
83 67
		if (null === $mangan)
84
		{
85 67
			$mangan = Mangan::fromModel($model);
86
		}
87 67
		$this->em = $em ?: EntityManager::create($model, $mangan);
88 67
		$this->mn = $mangan;
89 67
		$this->withCursor($this->mn->useCursor);
90 67
	}
91
92
	/**
93
	 * Create model related finder.
94
	 * This will create customized finder if defined in model with Finder annotation.
95
	 * If no custom finder is defined this will return default Finder.
96
	 *
97
	 * @param AnnotatedInterface $model
98
	 * @param EntityManagerInterface $em
99
	 * @param Mangan $mangan
100
	 * @return FinderInterface
101
	 */
102 9
	public static function create(AnnotatedInterface $model, $em = null, Mangan $mangan = null)
103
	{
104 9
		$finderClass = ManganMeta::create($model)->type()->finder ?: static::class;
105 9
		return new $finderClass($model, $em, $mangan);
106
	}
107
108
	/**
109
	 * Finds a single Document with the specified condition.
110
	 *
111
	 * @param array|CriteriaInterface $criteria query criteria.
112
	 * @return AnnotatedInterface
113
	 * @Ignored
114
	 */
115 52
	public function find($criteria = null)
116
	{
117 52
		if ($this->_beforeFind())
118
		{
119 52
			$criteria = $this->sm->apply($criteria);
120 52
			$criteria->decorateWith($this->model);
121 52
			$data = $this->em->getCollection()->findOne($criteria->getConditions(), $criteria->getSelect());
122 52
			return $this->populateRecord($data);
123
		}
124
		return null;
125
	}
126
127
	/**
128
	 * Finds document with the specified primary key. Primary key by default
129
	 * is defined by `_id` field. But could be any other. For simple (one column)
130
	 * keys use it's value.
131
	 *
132
	 * For composite use key-value with column names as keys
133
	 * and values for values.
134
	 *
135
	 * Example for simple pk:
136
	 * ```php
137
	 * $pk = '51b616fcc0986e30026d0748'
138
	 * ```
139
	 *
140
	 * Composite pk:
141
	 * ```php
142
	 * $pk = [
143
	 * 		'mainPk' => 1,
144
	 * 		'secondaryPk' => 2
145
	 * ];
146
	 * ```
147
	 *
148
	 * @param mixed $pkValue primary key value. Use array for composite key.
149
	 * @param array|CriteriaInterface $criteria
150
	 * @return AnnotatedInterface|null
151
	 * @Ignored
152
	 */
153 40
	public function findByPk($pkValue, $criteria = null)
154
	{
155
156 40
		$pkCriteria = new Criteria($criteria);
157 40
		$pkCriteria->decorateWith($this->model);
158 40
		$pkCriteria->mergeWith(PkManager::prepare($this->model, $pkValue));
159 40
		return $this->find($pkCriteria);
160
	}
161
162
	/**
163
	 * Finds document with the specified attributes.
164
	 * Attributes should be specified as key-value pairs.
165
	 * This allows easier syntax for simple queries.
166
	 *
167
	 * Example:
168
	 * ```php
169
	 * $attributes = [
170
	 * 		'name' => 'John',
171
	 * 		'title' => 'dr'
172
	 * ];
173
	 * ```
174
	 *
175
	 * @param mixed[] Array of stributes and values in form of ['attributeName' => 'value']
176
	 * @return AnnotatedInterface|null
177
	 */
178 2
	public function findByAttributes(array $attributes)
179
	{
180 2
		$criteria = new Criteria();
181 2
		$criteria->decorateWith($this->model);
182 2
		foreach ($attributes as $name => $value)
183
		{
184 2
			$criteria->addCond($name, '==', $value);
185
		}
186 2
		return $this->find($criteria);
187
	}
188
189
	/**
190
	 * Finds all documents satisfying the specified condition.
191
	 * See {@link find()} for detailed explanation about $condition and $params.
192
	 *
193
	 * @param array|CriteriaInterface $criteria query criteria.
194
	 * @return AnnotatedInterface[]|Cursor
195
	 */
196 18
	public function findAll($criteria = null)
197
	{
198 18
		if ($this->_beforeFind())
199
		{
200 18
			$criteria = $this->sm->apply($criteria);
201 18
			$criteria->decorateWith($this->model);
202 18
			$cursor = $this->em->getCollection()->find($criteria->getConditions());
203
204 18
			if ($criteria->getSort() !== null)
205
			{
206 18
				$cursor->sort($criteria->getSort());
207
			}
208 18
			if ($criteria->getLimit() !== null)
209
			{
210 1
				$cursor->limit($criteria->getLimit());
211
			}
212 18
			if ($criteria->getOffset() !== null)
213
			{
214 1
				$cursor->skip($criteria->getOffset());
215
			}
216 18
			if ($criteria->getSelect())
217
			{
218 1
				$cursor->fields($criteria->getSelect());
219
			}
220 18
			$this->mn->getProfiler()->cursor($cursor);
221 18
			if ($this->_useCursor)
222
			{
223 1
				return new Cursor($cursor, $this->model);
224
			}
225
			else
226
			{
227 17
				return $this->populateRecords($cursor);
228
			}
229
		}
230
		return [];
231
	}
232
233
	/**
234
	 * Finds all documents with the specified attributes.
235
	 *
236
	 * @param mixed[] Array of stributes and values in form of ['attributeName' => 'value']
237
	 * @return AnnotatedInterface[]|Cursor - Array or cursor of Documents
238
	 * @since v1.0
239
	 */
240 1
	public function findAllByAttributes(array $attributes)
241
	{
242 1
		$criteria = new Criteria();
243 1
		$criteria->decorateWith($this->model);
244 1
		foreach ($attributes as $name => $value)
245
		{
246 1
			$criteria->$name('==', $value);
247
		}
248
249 1
		return $this->findAll($criteria);
250
	}
251
252
	/**
253
	 * Finds all documents with the specified primary keys.
254
	 * In MongoDB world every document has '_id' unique field, so with this method that
255
	 * field is in use as PK by default.
256
	 * See {@link find()} for detailed explanation about $condition.
257
	 *
258
	 * @param mixed $pkValues primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value).
259
	 * @param array|CriteriaInterface $criteria query criteria.
260
	 * @return AnnotatedInterface[]|Cursor - Array or cursor of Documents
261
	 * @since v1.0
262
	 */
263 7
	public function findAllByPk($pkValues, $criteria = null)
264
	{
265 7
		$pkCriteria = new Criteria($criteria);
266 7
		$pkCriteria->decorateWith($this->model);
267 7
		PkManager::prepareAll($this->model, $pkValues, $pkCriteria);
268
269 7
		return $this->findAll($pkCriteria);
270
	}
271
272
	/**
273
	 * Counts all documents satisfying the specified condition.
274
	 * See {@link find()} for detailed explanation about $condition and $params.
275
	 * @param array|CriteriaInterface $criteria query criteria.
276
	 * @return integer Count of all documents satisfying the specified condition.
277
	 * @since v1.0
278
	 */
279 19
	public function count($criteria = null)
280
	{
281 19
		if ($this->_beforeCount())
282
		{
283 19
			$criteria = $this->sm->apply($criteria);
284 19
			$criteria->decorateWith($this->model);
285 19
			$count = $this->em->getCollection()->count($criteria->getConditions());
286 19
			Event::trigger($this->model, FinderInterface::EventAfterCount);
287 19
			return $count;
288
		}
289
		return 0;
290
	}
291
292
	/**
293
	 * Counts all documents found by attribute values.
294
	 *
295
	 * Example:
296
	 * ```php
297
	 * $attributes = [
298
	 * 		'name' => 'John',
299
	 * 		'title' => 'dr'
300
	 * ];
301
	 * ```
302
	 *
303
	 * @param mixed[] Array of attributes and values in form of ['attributeName' => 'value']
304
	 * @return int
305
	 * @since v1.2.2
306
	 * @Ignored
307
	 */
308 1
	public function countByAttributes(array $attributes)
309
	{
310 1
		if ($this->_beforeCount())
311
		{
312 1
			$criteria = new Criteria;
313 1
			$criteria->decorateWith($this->model);
314 1
			foreach ($attributes as $name => $value)
315
			{
316 1
				$criteria->$name = $value;
317
			}
318
319 1
			$criteria = $this->sm->apply($criteria);
320
321 1
			$count = $this->em->getCollection()->count($criteria->getConditions());
322 1
			Event::trigger($this->model, FinderInterface::EventAfterCount);
323 1
			return $count;
324
		}
325
		return 0;
326
	}
327
328
	/**
329
	 * Checks whether there is document satisfying the specified condition.
330
	 *
331
	 * @param CriteriaInterface $criteria
332
	 * @return bool
333
	 */
334 7
	public function exists(CriteriaInterface $criteria = null)
335
	{
336 7
		if ($this->_beforeExists())
337
		{
338 7
			$criteria = $this->sm->apply($criteria);
339 7
			$criteria->decorateWith($this->model);
340
			/**
341
			 * TODO Use as limited fields set as possible here:
342
			 * Pk Fields, or only id, or fields used as query, still need to investigate
343
			 */
344 7
			$cursor = $this->em->getCollection()->find($criteria->getConditions());
345 7
			$cursor->limit(1);
346
347
			// NOTE: Cannot use count(true) here because of hhvm mongofill compatibility, see:
348
			// https://github.com/mongofill/mongofill/issues/86
349 7
			$exists = ($cursor->count() !== 0);
350 7
			Event::trigger($this->model, FinderInterface::EventAfterExists);
351 7
			return $exists;
352
		}
353
		return false;
354
	}
355
356
	/**
357
	 * Whenever to use cursor
358
	 * @param bool $useCursor
359
	 * @return FinderInterface
360
	 */
361 67
	public function withCursor($useCursor = true)
362
	{
363 67
		$this->_useCursor = $useCursor;
364 67
		return $this;
365
	}
366
367
	/**
368
	 * Resets all scopes and criteria applied including default scope.
369
	 *
370
	 * @return Finder
371
	 * @since v1.0
372
	 */
373
	public function resetScope()
374
	{
375
		$this->_criteria = new Criteria();
376
		$this->_criteria->decorateWith($this->model);
377
		return $this;
378
	}
379
380
	/**
381
	 * Creates an model with the given attributes.
382
	 * This method is internally used by the find methods.
383
	 * @param mixed[] $data attribute values (column name=>column value)
384
	 * @return AnnotatedInterface|null the newly created document. The class of the object is the same as the model class.
385
	 * Null is returned if the input data is false.
386
	 * @since v1.0
387
	 */
388 59
	protected function populateRecord($data)
389
	{
390 59
		if ($data !== null)
391
		{
392 58
			if (!empty($data['$err']))
393
			{
394
				throw new ManganException(sprintf("There is an error in query: %s", $data['$err']));
395
			}
396 58
			$model = RawArray::toModel($data, $this->model);
397 58
			ScenarioManager::setScenario($model, ScenariosInterface::Update);
398 58
			Event::trigger($model, FinderInterface::EventAfterFind);
399 58
			return $model;
400
		}
401
		else
402
		{
403 3
			return null;
404
		}
405
	}
406
407
	/**
408
	 * Creates a list of documents based on the input data.
409
	 * This method is internally used by the find methods.
410
	 * @param MongoCursor $cursor Results found to populate active records.
411
	 * @return AnnotatedInterface[] array list of active records.
412
	 * @since v1.0
413
	 */
414 17
	protected function populateRecords(MongoCursor $cursor)
415
	{
416 17
		$records = array();
417 17
		foreach ($cursor as $data)
418
		{
419 17
			$records[] = $this->populateRecord($data);
420
		}
421 17
		return $records;
422
	}
423
424
	/**
425
	 * Trigger before find event
426
	 * @return boolean
427
	 */
428 60
	private function _beforeFind()
429
	{
430 60
		if (!Event::hasHandler($this->model, FinderInterface::EventBeforeFind))
431
		{
432 60
			return true;
433
		}
434
		return Event::handled($this->model, FinderInterface::EventBeforeFind);
435
	}
436
437
	/**
438
	 * Trigger before count event
439
	 * @return boolean
440
	 */
441 19
	private function _beforeCount()
442
	{
443 19
		if (!Event::hasHandler($this->model, FinderInterface::EventBeforeCount))
444
		{
445 19
			return true;
446
		}
447
		return Event::handled($this->model, FinderInterface::EventBeforeCount);
448
	}
449
450
	/**
451
	 * Trigger before exists event
452
	 * @return boolean
453
	 */
454 7
	private function _beforeExists()
455
	{
456 7
		if (!Event::hasHandler($this->model, FinderInterface::EventBeforeExists))
457
		{
458 7
			return true;
459
		}
460
		return Event::handled($this->model, FinderInterface::EventBeforeExists);
461
	}
462
463
}
464