Completed
Push — master ( 0a999d...b4b35b )
by Peter
13:24 queued 02:45
created

EntityManager::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.004

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 12
cts 13
cp 0.9231
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 4
nop 2
crap 3.004
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\Events\Event;
18
use Maslosoft\Mangan\Events\EventDispatcher;
19
use Maslosoft\Mangan\Events\ModelEvent;
20
use Maslosoft\Mangan\Exceptions\ManganException;
21
use Maslosoft\Mangan\Helpers\CollectionNamer;
22
use Maslosoft\Mangan\Helpers\PkManager;
23
use Maslosoft\Mangan\Interfaces\CriteriaInterface;
24
use Maslosoft\Mangan\Interfaces\EntityManagerInterface;
25
use Maslosoft\Mangan\Interfaces\ScenariosInterface;
26
use Maslosoft\Mangan\Meta\ManganMeta;
27
use Maslosoft\Mangan\Options\EntityOptions;
28
use Maslosoft\Mangan\Signals\AfterDelete;
29
use Maslosoft\Mangan\Signals\AfterSave;
30
use Maslosoft\Mangan\Signals\BeforeDelete;
31
use Maslosoft\Mangan\Signals\BeforeSave;
32
use Maslosoft\Mangan\Transformers\RawArray;
33
use Maslosoft\Mangan\Transformers\SafeArray;
34
use Maslosoft\Signals\Signal;
35
use MongoCollection;
36
37
/**
38
 * EntityManager
39
 * 
40
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
41
 */
42
class EntityManager implements EntityManagerInterface
43
{
44
45
	/**
46
	 * Model
47
	 * @var AnnotatedInterface
48
	 */
49
	public $model = null;
50
51
	/**
52
	 *
53
	 * @var EventDispatcher
54
	 */
55
	public $ed = null;
56
57
	/**
58
	 *
59
	 * @var ScopeManager
60
	 */
61
	private $sm = null;
62
63
	/**
64
	 *
65
	 * @var
66
	 */
67
	public $meta = null;
68
69
	/**
70
	 * Options
71
	 * @var EntityOptions
72
	 */
73
	public $options = null;
74
75
	/**
76
	 * Current collection name
77
	 * @var string
78
	 */
79
	public $collectionName = '';
80
81
	/**
82
	 * Validator instance
83
	 * @var Validator
84
	 */
85
	private $validator = null;
86
87
	/**
88
	 * Current collection
89
	 * @var MongoCollection
90
	 */
91
	private $_collection = null;
92
93
	/**
94
	 * Create entity manager
95
	 * @param AnnotatedInterface $model
96
	 * @param Mangan $mangan
97
	 * @throws ManganException
98
	 */
99 94
	public function __construct(AnnotatedInterface $model, Mangan $mangan = null)
100
	{
101 94
		$this->model = $model;
102 94
		$this->sm = new ScopeManager($model);
103 94
		$this->options = new EntityOptions($model);
104 94
		$this->collectionName = CollectionNamer::nameCollection($model);
105 94
		$this->meta = ManganMeta::create($model);
106 94
		$this->validator = new Validator($model);
107 94
		if (null === $mangan)
108
		{
109 93
			$mangan = Mangan::fromModel($model);
110
		}
111 94
		if (!$this->collectionName)
112
		{
113
			throw new ManganException(sprintf('Invalid collection name for model: `%s`', $this->meta->type()->name));
114
		}
115 94
		$this->_collection = new MongoCollection($mangan->getDbInstance(), $this->collectionName);
116 94
	}
117
118
	/**
119
	 * Create model related entity manager.
120
	 * This will create customized entity manger if defined in model with EntityManager annotation.
121
	 * If no custom entity manager is defined this will return default EntityManager.
122
	 * @param AnnotatedInterface $model
123
	 * @param Mangan $mangan
124
	 * @return EntityManagerInterface
125
	 */
126 86
	public static function create($model, Mangan $mangan = null)
127
	{
128 86
		$emClass = ManganMeta::create($model)->type()->entityManager ?: static::class;
129 86
		return new $emClass($model, $mangan);
130
	}
131
132
	/**
133
	 * Set attributes en masse.
134
	 * Attributes will be filtered according to SafeAnnotation.
135
	 * Only attributes marked as safe will be set, other will be ignored.
136
	 *
137
	 * @param mixed[] $atributes
138
	 */
139
	public function setAttributes($atributes)
140
	{
141
		SafeArray::toModel($atributes, $this->model, $this->model);
142
	}
143
144
	/**
145
	 * Inserts a row into the table based on this active record attributes.
146
	 * If the table's primary key is auto-incremental and is null before insertion,
147
	 * it will be populated with the actual value after insertion.
148
	 *
149
	 * Note, validation is not performed in this method. You may call {@link validate} to perform the validation.
150
	 * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false,
151
	 * and its {@link scenario} property will be set to be 'update'.
152
	 *
153
	 * @param AnnotatedInterface $model if want to insert different model than set in constructor
154
	 *
155
	 * @return boolean whether the attributes are valid and the record is inserted successfully.
156
	 * @throws ManganException if the record is not new
157
	 * @throws ManganException on fail of insert or insert of empty document
158
	 * @throws ManganException on fail of insert, when safe flag is set to true
159
	 * @throws ManganException on timeout of db operation , when safe flag is set to true
160
	 * @since v1.0
161
	 */
162 35
	public function insert(AnnotatedInterface $model = null)
163
	{
164 35
		$model = $model ?: $this->model;
165 35
		if ($this->_beforeSave($model, EntityManagerInterface::EventBeforeInsert))
166
		{
167 35
			$rawData = RawArray::fromModel($model);
168 35
			$rawResult = $this->_collection->insert($rawData, $this->options->getSaveOptions());
169 35
			$result = $this->_result($rawResult, true);
170
171 35
			if ($result)
172
			{
173 35
				$this->_afterSave($model, EntityManagerInterface::EventAfterInsert);
174 35
				return true;
175
			}
176
			throw new ManganException('Can\t save the document to disk, or attempting to save an empty document.');
177
		}
178
		return false;
179
	}
180
181
	/**
182
	 * Updates the row represented by this active document.
183
	 * All loaded attributes will be saved to the database.
184
	 * Note, validation is not performed in this method. You may call {@link validate} to perform the validation.
185
	 *
186
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
187
	 * meaning all attributes that are loaded from DB will be saved.
188
189
	 * @return boolean whether the update is successful
190
	 * @throws ManganException if the record is new
191
	 * @throws ManganException on fail of update
192
	 * @throws ManganException on timeout of db operation , when safe flag is set to true
193
	 * @since v1.0
194
	 */
195 5
	public function update(array $attributes = null)
196
	{
197 5
		if ($this->_beforeSave($this->model, EntityManagerInterface::EventBeforeUpdate))
198
		{
199 5
			$criteria = PkManager::prepareFromModel($this->model);
200 5
			$result = $this->updateOne($criteria, $attributes);
201 5
			if ($result)
202
			{
203 5
				$this->_afterSave($this->model, EntityManagerInterface::EventAfterUpdate);
204 5
				return true;
205
			}
206
			throw new ManganException('Can\t save the document to disk, or attempting to save an empty document.');
207
		}
208
		return false;
209
	}
210
211
	/**
212
	 * Updates one document with the specified criteria and attributes
213
	 *
214
	 * This is more *raw* update:
215
	 *
216
	 * * Does not raise any events or signals
217
	 * * Does not perform any validation
218
	 *
219
	 * @param array|CriteriaInterface $criteria query criteria.
220
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
221
	 * @param bool Whether tu force update/upsert document
222
	 * meaning all attributes that are loaded from DB will be saved.
223
	 * @since v1.0
224
	 */
225 60
	public function updateOne($criteria = null, array $attributes = null, $modify = false)
226
	{
227 60
		$criteria = $this->sm->apply($criteria);
228 60
		$rawData = RawArray::fromModel($this->model);
229
230
		// filter attributes if set in param
231 60
		if ($attributes !== null)
232
		{
233 1
			$modify = true;
234 1
			foreach ($rawData as $key => $value)
235
			{
236 1
				if (!in_array($key, $attributes))
237
				{
238 1
					unset($rawData[$key]);
239
				}
240
			}
241
		}
242
		else
243
		{
244 60
			$fields = array_keys(ManganMeta::create($this->model)->fields());
245 60
			$setFields = array_keys($rawData);
246 60
			$diff = array_diff($fields, $setFields);
247
248 60
			if (!empty($diff))
249
			{
250 5
				$modify = true;
251
			}
252
		}
253 60
		if ($modify)
254
		{
255
			// Id could be altered, so skip it as it cannot be changed
256 6
			unset($rawData['_id']);
257 6
			$data = ['$set' => $rawData];
258
		}
259
		else
260
		{
261 57
			$data = $rawData;
262
		}
263
264 60
		$result = $this->getCollection()->update($criteria->getConditions(), $data, $this->options->getSaveOptions(['multiple' => false, 'upsert' => true]));
265 60
		return $this->_result($result);
266
	}
267
268
	/**
269
	 * Atomic, in-place update method.
270
	 *
271
	 * @since v1.3.6
272
	 * @param Modifier $modifier updating rules to apply
273
	 * @param CriteriaInterface $criteria condition to limit updating rules
274
	 * @return boolean
275
	 */
276 1
	public function updateAll(Modifier $modifier, CriteriaInterface $criteria = null)
277
	{
278 1
		if ($modifier->canApply())
279
		{
280 1
			$criteria = $this->sm->apply($criteria);
281 1
			$result = $this->getCollection()->update($criteria->getConditions(), $modifier->getModifiers(), $this->options->getSaveOptions([
282 1
						'upsert' => false,
283
						'multiple' => true
284
			]));
285 1
			return $this->_result($result);
286
		}
287
		else
288
		{
289
			return false;
290
		}
291
	}
292
293
	/**
294
	 * Replaces the current document.
295
	 *
296
	 * **NOTE: This will overwrite entire document.**
297
	 * Any filtered out properties will be removed as well.
298
	 *
299
	 * The record is inserted as a documnent into the database collection, if exists it will be replaced.
300
	 *
301
	 * Validation will be performed before saving the record. If the validation fails,
302
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
303
	 * validation errors.
304
	 *
305
	 * @param boolean $runValidation whether to perform validation before saving the record.
306
	 * If the validation fails, the record will not be saved to database.
307
	 *
308
	 * @return boolean whether the saving succeeds
309
	 * @since v1.0
310
	 */
311
	public function replace($runValidation = true)
312
	{
313
		if (!$runValidation || $this->validator->validate())
314
		{
315
			$model = $this->model;
316
			if ($this->_beforeSave($model))
317
			{
318
				$data = RawArray::fromModel($model);
319
				$rawResult = $this->_collection->save($data, $this->options->getSaveOptions());
320
				$result = $this->_result($rawResult, true);
321
322
				if ($result)
323
				{
324
					$this->_afterSave($model);
325
					return true;
326
				}
327
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document");
328
			}
329
			return false;
330
		}
331
		else
332
		{
333
			return false;
334
		}
335
	}
336
337
	/**
338
	 * Saves the current document.
339
	 *
340
	 * The record is inserted as a document into the database collection or updated if exists.
341
	 *
342
	 * Filtered out properties will remain in database - it is partial safe.
343
	 *
344
	 * Validation will be performed before saving the record. If the validation fails,
345
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
346
	 * validation errors.
347
	 *
348
	 * @param boolean $runValidation whether to perform validation before saving the record.
349
	 * If the validation fails, the record will not be saved to database.
350
	 *
351
	 * @return boolean whether the saving succeeds
352
	 * @since v1.0
353
	 */
354 59
	public function save($runValidation = true)
355
	{
356 59
		return $this->upsert($runValidation);
357
	}
358
359
	/**
360
	 * Updates or inserts the current document. This will try to update existing fields.
361
	 * Will keep already stored data if present in document.
362
	 *
363
	 * If document does not exist, a new one will be inserted.
364
	 *
365
	 * @param boolean $runValidation
366
	 * @return boolean
367
	 * @throws ManganException
368
	 */
369 61
	public function upsert($runValidation = true)
370
	{
371 61
		if (!$runValidation || $this->validator->validate())
372
		{
373 61
			$model = $this->model;
374 61
			if ($this->_beforeSave($model))
375
			{
376 58
				$criteria = PkManager::prepareFromModel($this->model);
377 58
				foreach ($criteria->getConditions() as $field => $value)
378
				{
379 58
					if (empty($this->model->$field))
380
					{
381 58
						$this->model->$field = $value;
382
					}
383
				}
384 58
				$result = $this->updateOne($criteria);
385
386 58
				if ($result)
387
				{
388 58
					$this->_afterSave($model);
389 58
					return true;
390
				}
391
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document");
392
			}
393 3
			return false;
394
		}
395
		else
396
		{
397 2
			return false;
398
		}
399
	}
400
401
	/**
402
	 * Reloads document from database.
403
	 * It return true if document is reloaded and false if it's no longer exists.
404
	 *
405
	 * @return boolean
406
	 */
407 2
	public function refresh()
408
	{
409 2
		$conditions = PkManager::prepareFromModel($this->model)->getConditions();
410 2
		$data = $this->getCollection()->findOne($conditions);
411 2
		if (null !== $data)
412
		{
413 2
			RawArray::toModel($data, $this->model, $this->model);
414 2
			return true;
415
		}
416
		else
417
		{
418 2
			return false;
419
		}
420
	}
421
422
	/**
423
	 * Deletes the document from database.
424
	 * @return boolean whether the deletion is successful.
425
	 * @throws ManganException if the record is new
426
	 */
427 10
	public function delete()
428
	{
429 10
		if ($this->_beforeDelete())
430
		{
431 9
			$result = $this->deleteOne(PkManager::prepareFromModel($this->model));
432
433 9
			if ($result !== false)
434
			{
435 9
				$this->_afterDelete();
436 9
				return true;
437
			}
438
			else
439
			{
440 1
				return false;
441
			}
442
		}
443
		else
444
		{
445 1
			return false;
446
		}
447
	}
448
449
	/**
450
	 * Deletes one document with the specified primary keys.
451
	 * <b>Does not raise beforeDelete</b>
452
	 * See {@link find()} for detailed explanation about $condition and $params.
453
	 * @param array|CriteriaInterface $criteria query criteria.
454
	 * @since v1.0
455
	 */
456 11
	public function deleteOne($criteria = null)
457
	{
458 11
		$criteria = $this->sm->apply($criteria);
459
460 11
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
461
					'justOne' => true
462 11
		]));
463 11
		return $this->_result($result);
464
	}
465
466
	/**
467
	 * Deletes document with the specified primary key.
468
	 * See {@link find()} for detailed explanation about $condition and $params.
469
	 * @param mixed $pkValue primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value).
470
	 * @param array|CriteriaInterface $criteria query criteria.
471
	 * @since v1.0
472
	 */
473 2
	public function deleteByPk($pkValue, $criteria = null)
474
	{
475 2
		if ($this->_beforeDelete())
476
		{
477 1
			$criteria = $this->sm->apply($criteria);
478 1
			$criteria->mergeWith(PkManager::prepare($this->model, $pkValue));
479
480 1
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
481
						'justOne' => true
482 1
			]));
483 1
			return $this->_result($result);
484
		}
485 1
		return false;
486
	}
487
488
	/**
489
	 * Deletes documents with the specified primary keys.
490
	 * See {@link find()} for detailed explanation about $condition and $params.
491
	 * @param mixed[] $pkValues Primary keys array
492
	 * @param array|CriteriaInterface $criteria query criteria.
493
	 * @since v1.0
494
	 */
495 2
	public function deleteAllByPk($pkValues, $criteria = null)
496
	{
497 2
		if ($this->_beforeDelete())
498
		{
499 1
			$criteria = $this->sm->apply($criteria);
500 1
			$criteria->mergeWith(PkManager::prepareAll($this->model, $pkValues, $criteria));
501 1
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
502
						'justOne' => false
503 1
			]));
504 1
			return $this->_result($result);
505
		}
506 1
		return false;
507
	}
508
509
	/**
510
	 * Deletes documents with the specified primary keys.
511
	 *
512
	 * **Does not raise beforeDelete event and does not emit signals**
513
	 *
514
	 * See {@link find()} for detailed explanation about $condition and $params.
515
	 *
516
	 * @param array|CriteriaInterface $criteria query criteria.
517
	 * @since v1.0
518
	 */
519 7
	public function deleteAll($criteria = null)
520
	{
521 7
		$criteria = $this->sm->apply($criteria);
522
523 7
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
524
					'justOne' => false
525 7
		]));
526 7
		return $this->_result($result);
527
	}
528
529 89
	public function getCollection()
530
	{
531 89
		return $this->_collection;
532
	}
533
534
	/**
535
	 * Make status uniform
536
	 * @param bool|array $result
537
	 * @param bool $insert Set to true for inserts
538
	 * @return bool Return true if secceed
539
	 */
540 90
	private function _result($result, $insert = false)
541
	{
542 90
		if (is_array($result))
543
		{
544 90
			if ($insert)
545
			{
546 35
				return (bool) $result['ok'];
547
			}
548 60
			return (bool) $result['n'];
549
		}
550
		return $result;
551
	}
552
553
// <editor-fold defaultstate="collapsed" desc="Event and Signal handling">
554
555
	/**
556
	 * Take care of EventBeforeSave
557
	 * @see EventBeforeSave
558
	 * @return boolean
559
	 */
560 92
	private function _beforeSave($model, $event = null)
561
	{
562 92
		$result = Event::Valid($model, EntityManagerInterface::EventBeforeSave);
563 92
		if ($result)
564
		{
565 89
			if (!empty($event))
566
			{
567 40
				Event::trigger($model, $event);
568
			}
569 89
			(new Signal)->emit(new BeforeSave($model));
570
		}
571 92
		return $result;
572
	}
573
574
	/**
575
	 * Take care of EventAfterSave
576
	 * @see EventAfterSave
577
	 */
578 89
	private function _afterSave($model, $event = null)
579
	{
580 89
		Event::trigger($model, EntityManagerInterface::EventAfterSave);
581 89
		if (!empty($event))
582
		{
583 40
			Event::trigger($model, $event);
584
		}
585 89
		(new Signal)->emit(new AfterSave($model));
586 89
		ScenarioManager::setScenario($model, ScenariosInterface::Update);
587 89
	}
588
589
	/**
590
	 * This method is invoked before deleting a record.
591
	 * The default implementation raises the {@link onBeforeDelete} event.
592
	 * You may override this method to do any preparation work for record deletion.
593
	 * Make sure you call the parent implementation so that the event is raised properly.
594
	 * @return boolean whether the record should be deleted. Defaults to true.
595
	 * @since v1.0
596
	 */
597 12
	private function _beforeDelete()
598
	{
599 12
		$result = Event::valid($this->model, EntityManagerInterface::EventBeforeDelete);
600 12
		if ($result)
601
		{
602 11
			(new Signal)->emit(new BeforeDelete($this->model));
603 11
			ScenarioManager::setScenario($this->model, ScenariosInterface::Delete);
604
		}
605 12
		return $result;
606
	}
607
608
	/**
609
	 * This method is invoked after deleting a record.
610
	 * The default implementation raises the {@link onAfterDelete} event.
611
	 * You may override this method to do postprocessing after the record is deleted.
612
	 * Make sure you call the parent implementation so that the event is raised properly.
613
	 * @since v1.0
614
	 */
615 9
	private function _afterDelete()
616
	{
617 9
		$event = new ModelEvent($this->model);
618 9
		Event::trigger($this->model, EntityManagerInterface::EventAfterDelete, $event);
619 9
		(new Signal)->emit(new AfterDelete($this->model));
620 9
	}
621
622
// </editor-fold>
623
}
624