Completed
Push — master ( 0310e6...e3b97d )
by Peter
24:20 queued 21:09
created

EntityManager::setAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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