Completed
Push — master ( e9b8ec...943b9d )
by Peter
55:00
created

EntityManager::update()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.576

Importance

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