Completed
Push — master ( 507c4a...51e00b )
by Peter
07:40
created

EntityManager::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 2
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
	 * Result of last operation
89
	 * @var array
90
	 */
91
	private $lastResult = [];
92
93
	/**
94
	 * Create entity manager
95
	 * @param AnnotatedInterface $model
96
	 * @param Mangan $mangan
97
	 * @throws ManganException
98
	 */
99 118
	public function __construct(AnnotatedInterface $model, Mangan $mangan = null)
100
	{
101 118
		$this->model = $model;
102 118
		$this->sm = new ScopeManager($model);
103 118
		$this->options = new EntityOptions($model);
104 118
		$this->collectionName = CollectionNamer::nameCollection($model);
105 118
		$this->meta = ManganMeta::create($model);
106 118
		$this->validator = new Validator($model);
107 118
		if (null === $mangan)
108
		{
109 115
			$mangan = Mangan::fromModel($model);
110
		}
111 118
		if (!$this->collectionName)
112
		{
113
			throw new ManganException(sprintf('Invalid collection name for model: `%s`', $this->meta->type()->name));
114
		}
115 118
		$this->_collection = new MongoCollection($mangan->getDbInstance(), $this->collectionName);
116 118
	}
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 109
	public static function create($model, Mangan $mangan = null)
127
	{
128 109
		$emClass = ManganMeta::create($model)->type()->entityManager ?: static::class;
129 109
		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 36
	public function insert(AnnotatedInterface $model = null)
163
	{
164 36
		$model = $model ?: $this->model;
165 36
		if ($this->_beforeSave($model, EntityManagerInterface::EventBeforeInsert))
166
		{
167 36
			$rawData = RawArray::fromModel($model);
168
			/**
169
			 * TODO Save options ara failing with message:
170
			 * Unrecognized write concern field: authMechanism
171
			 * $this->options->getSaveOptions()
172
			 */
173 36
			$rawResult = $this->_collection->insert($rawData);
174 36
			$result = $this->_result($rawResult, true);
175
176 36
			if ($result)
177
			{
178 36
				$this->_afterSave($model, EntityManagerInterface::EventAfterInsert);
179 36
				return true;
180
			}
181
			codecept_debug($rawResult);
182
			throw new ManganException('Can\t save the document to disk, or attempting to save an empty document. ' . ucfirst($rawResult['errmsg']), $rawResult['code']);
183
		}
184
		return false;
185
	}
186
187
	/**
188
	 * Updates the row represented by this active document.
189
	 * All loaded attributes will be saved to the database.
190
	 * Note, validation is not performed in this method. You may call {@link validate} to perform the validation.
191
	 *
192
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
193
	 * meaning all attributes that are loaded from DB will be saved.
194
195
	 * @return boolean whether the update is successful
196
	 * @throws ManganException if the record is new
197
	 * @throws ManganException on fail of update
198
	 * @throws ManganException on timeout of db operation , when safe flag is set to true
199
	 * @since v1.0
200
	 */
201 6
	public function update(array $attributes = null)
202
	{
203 6
		if ($this->_beforeSave($this->model, EntityManagerInterface::EventBeforeUpdate))
204
		{
205 6
			$criteria = PkManager::prepareFromModel($this->model);
206 6
			$result = $this->updateOne($criteria, $attributes);
207 5
			if ($result)
208
			{
209 5
				$this->_afterSave($this->model, EntityManagerInterface::EventAfterUpdate);
210 5
				return true;
211
			}
212
			throw new ManganException('Can\t save the document to disk, or attempting to save an empty document.');
213
		}
214
		return false;
215
	}
216
217
	/**
218
	 * Updates one document with the specified criteria and attributes
219
	 *
220
	 * This is more *raw* update:
221
	 *
222
	 * * Does not raise any events or signals
223
	 * * Does not perform any validation
224
	 *
225
	 * @param array|CriteriaInterface $criteria query criteria.
226
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
227
	 * @param bool Whether tu force update/upsert document
228
	 * meaning all attributes that are loaded from DB will be saved.
229
	 * @since v1.0
230
	 */
231 81
	public function updateOne($criteria = null, array $attributes = null, $modify = false)
232
	{
233 81
		$criteria = $this->sm->apply($criteria);
234 81
		$rawData = RawArray::fromModel($this->model);
235
236
		// filter attributes if set in param
237 81
		if ($attributes !== null)
238
		{
239 2
			$meta = ManganMeta::create($this->model);
240 2
			foreach ($attributes as $key)
241
			{
242 2
				if ($meta->$key === false)
243
				{
244 2
					throw new BadAttributeException(sprintf("Unknown attribute `%s` on model `%s`", $key, get_class($this->model)));
245
				}
246
			}
247 1
			$modify = true;
248 1
			foreach ($rawData as $key => $value)
249
			{
250 1
				if (!in_array($key, $attributes))
251
				{
252 1
					unset($rawData[$key]);
253
				}
254
			}
255
		}
256
		else
257
		{
258 81
			$fields = array_keys(ManganMeta::create($this->model)->fields());
259 81
			$setFields = array_keys($rawData);
260 81
			$diff = array_diff($fields, $setFields);
261
262 81
			if (!empty($diff))
263
			{
264 6
				$modify = true;
265
			}
266
		}
267 81
		if ($modify)
268
		{
269
			// Id could be altered, so skip it as it cannot be changed
270 7
			unset($rawData['_id']);
271 7
			$data = ['$set' => $rawData];
272
		}
273
		else
274
		{
275 78
			$data = $rawData;
276
		}
277
278 81
		$result = $this->getCollection()->update($criteria->getConditions(), $data, $this->options->getSaveOptions(['multiple' => false, 'upsert' => true]));
279 81
		return $this->_result($result);
280
	}
281
282
	/**
283
	 * Atomic, in-place update method.
284
	 *
285
	 * @since v1.3.6
286
	 * @param Modifier $modifier updating rules to apply
287
	 * @param CriteriaInterface $criteria condition to limit updating rules
288
	 * @return boolean
289
	 */
290 1
	public function updateAll(Modifier $modifier, CriteriaInterface $criteria = null)
291
	{
292 1
		if ($modifier->canApply())
293
		{
294 1
			$criteria = $this->sm->apply($criteria);
295 1
			$result = $this->getCollection()->update($criteria->getConditions(), $modifier->getModifiers(), $this->options->getSaveOptions([
296 1
						'upsert' => false,
297
						'multiple' => true
298
			]));
299 1
			return $this->_result($result);
300
		}
301
		else
302
		{
303
			return false;
304
		}
305
	}
306
307
	/**
308
	 * Replaces the current document.
309
	 *
310
	 * **NOTE: This will overwrite entire document.**
311
	 * Any filtered out properties will be removed as well.
312
	 *
313
	 * The record is inserted as a documnent into the database collection, if exists it will be replaced.
314
	 *
315
	 * Validation will be performed before saving the record. If the validation fails,
316
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
317
	 * validation errors.
318
	 *
319
	 * @param boolean $runValidation whether to perform validation before saving the record.
320
	 * If the validation fails, the record will not be saved to database.
321
	 *
322
	 * @return boolean whether the saving succeeds
323
	 * @since v1.0
324
	 */
325
	public function replace($runValidation = true)
326
	{
327
		if (!$runValidation || $this->validator->validate())
328
		{
329
			$model = $this->model;
330
			if ($this->_beforeSave($model))
331
			{
332
				$data = RawArray::fromModel($model);
333
				$rawResult = $this->_collection->save($data, $this->options->getSaveOptions());
334
				$result = $this->_result($rawResult, true);
335
336
				if ($result)
337
				{
338
					$this->_afterSave($model);
339
					return true;
340
				}
341
				$msg = '';
342
				if(!empty($rawResult['errmsg']))
343
				{
344
					$msg = $rawResult['errmsg'];
345
				}
346
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document. $msg");
347
			}
348
			return false;
349
		}
350
		else
351
		{
352
			return false;
353
		}
354
	}
355
356
	/**
357
	 * Saves the current document.
358
	 *
359
	 * The record is inserted as a document into the database collection or updated if exists.
360
	 *
361
	 * Filtered out properties will remain in database - it is partial safe.
362
	 *
363
	 * Validation will be performed before saving the record. If the validation fails,
364
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
365
	 * validation errors.
366
	 *
367
	 * @param boolean $runValidation whether to perform validation before saving the record.
368
	 * If the validation fails, the record will not be saved to database.
369
	 *
370
	 * @return boolean whether the saving succeeds
371
	 * @since v1.0
372
	 */
373 78
	public function save($runValidation = true)
374
	{
375 78
		return $this->upsert($runValidation);
376
	}
377
378
	/**
379
	 * Updates or inserts the current document. This will try to update existing fields.
380
	 * Will keep already stored data if present in document.
381
	 *
382
	 * If document does not exist, a new one will be inserted.
383
	 *
384
	 * @param boolean $runValidation
385
	 * @return boolean
386
	 * @throws ManganException
387
	 */
388 82
	public function upsert($runValidation = true)
389
	{
390 82
		if (!$runValidation || $this->validator->validate())
391
		{
392 82
			$model = $this->model;
393 82
			if ($this->_beforeSave($model))
394
			{
395 79
				$criteria = PkManager::prepareFromModel($this->model);
396 79
				foreach ($criteria->getConditions() as $field => $value)
397
				{
398 79
					if (empty($this->model->$field))
399
					{
400 79
						$this->model->$field = $value;
401
					}
402
				}
403 79
				$result = $this->updateOne($criteria);
404
405 79
				if ($result)
406
				{
407 79
					$this->_afterSave($model);
408 79
					return true;
409
				}
410
				$errmsg = '';
411
				if(!empty($this->lastResult['errmsg']))
412
				{
413
					$errmsg = ucfirst($this->lastResult['errmsg']) . '.';
414
				}
415
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document. $errmsg");
416
			}
417 3
			return false;
418
		}
419
		else
420
		{
421 3
			return false;
422
		}
423
	}
424
425
	/**
426
	 * Reloads document from database.
427
	 * It return true if document is reloaded and false if it's no longer exists.
428
	 *
429
	 * @return boolean
430
	 */
431 2
	public function refresh()
432
	{
433 2
		$conditions = PkManager::prepareFromModel($this->model)->getConditions();
434 2
		$data = $this->getCollection()->findOne($conditions);
435 2
		if (null !== $data)
436
		{
437 2
			RawArray::toModel($data, $this->model, $this->model);
438 2
			return true;
439
		}
440
		else
441
		{
442 2
			return false;
443
		}
444
	}
445
446
	/**
447
	 * Deletes the document from database.
448
	 * @return boolean whether the deletion is successful.
449
	 * @throws ManganException if the record is new
450
	 */
451 10
	public function delete()
452
	{
453 10
		if ($this->_beforeDelete())
454
		{
455 9
			$result = $this->deleteOne(PkManager::prepareFromModel($this->model));
456
457 9
			if ($result !== false)
458
			{
459 9
				$this->_afterDelete();
460 9
				return true;
461
			}
462
			else
463
			{
464 1
				return false;
465
			}
466
		}
467
		else
468
		{
469 1
			return false;
470
		}
471
	}
472
473
	/**
474
	 * Deletes one document with the specified primary keys.
475
	 * <b>Does not raise beforeDelete</b>
476
	 * See {@link find()} for detailed explanation about $condition and $params.
477
	 * @param array|CriteriaInterface $criteria query criteria.
478
	 * @since v1.0
479
	 */
480 11
	public function deleteOne($criteria = null)
481
	{
482 11
		$criteria = $this->sm->apply($criteria);
483
484 11
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
485 11
					'justOne' => true
486
		]));
487 11
		return $this->_result($result);
488
	}
489
490
	/**
491
	 * Deletes document with the specified primary key.
492
	 * See {@link find()} for detailed explanation about $condition and $params.
493
	 * @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).
494
	 * @param array|CriteriaInterface $criteria query criteria.
495
	 * @since v1.0
496
	 */
497 3
	public function deleteByPk($pkValue, $criteria = null)
498
	{
499 3
		if ($this->_beforeDelete())
500
		{
501 2
			$criteria = $this->sm->apply($criteria);
502 2
			$criteria->mergeWith(PkManager::prepare($this->model, $pkValue));
503
504 2
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
505 2
						'justOne' => true
506
			]));
507 2
			return $this->_result($result);
508
		}
509 1
		return false;
510
	}
511
512
	/**
513
	 * Deletes documents with the specified primary keys.
514
	 * See {@link find()} for detailed explanation about $condition and $params.
515
	 * @param mixed[] $pkValues Primary keys array
516
	 * @param array|CriteriaInterface $criteria query criteria.
517
	 * @since v1.0
518
	 * @return bool
519
	 */
520 3
	public function deleteAllByPk($pkValues, $criteria = null)
521
	{
522 3
		if ($this->_beforeDelete())
523
		{
524 2
			$criteria = $this->sm->apply($criteria);
525 2
			$criteria->mergeWith(PkManager::prepareAll($this->model, $pkValues, $criteria));
526 2
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
527 2
						'justOne' => false
528
			]));
529 2
			return $this->_result($result);
530
		}
531 1
		return false;
532
	}
533
534
	/**
535
	 * Deletes documents with the specified primary keys.
536
	 *
537
	 * **Does not raise beforeDelete event and does not emit signals**
538
	 *
539
	 * See {@link find()} for detailed explanation about $condition and $params.
540
	 *
541
	 * @param array|CriteriaInterface $criteria query criteria.
542
	 * @since v1.0
543
	 * @return bool
544
	 */
545 5
	public function deleteAll($criteria = null)
546
	{
547 5
		$criteria = $this->sm->apply($criteria);
548
549
		// NOTE: Do not use [justOne => false] here
550 5
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions());
551 5
		return $this->_result($result);
552
	}
553
554 113
	public function getCollection()
555
	{
556 113
		return $this->_collection;
557
	}
558
559
	/**
560
	 * Make status uniform
561
	 * @param bool|array $result
562
	 * @param bool $insert Set to true for inserts
563
	 * @return bool Return true if succeed
564
	 */
565 112
	private function _result($result, $insert = false)
566
	{
567 112
		$this->lastResult = $result;
0 ignored issues
show
Documentation Bug introduced by
It seems like $result can also be of type boolean. However, the property $lastResult is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
568 112
		if (is_array($result))
569
		{
570 112
			if ($insert)
571
			{
572 36
				return (bool) $result['ok'];
573
			}
574 81
			return (bool) $result['n'];
575
		}
576
		return $result;
577
	}
578
579
// <editor-fold defaultstate="collapsed" desc="Event and Signal handling">
580
581
	/**
582
	 * Take care of EventBeforeSave
583
	 * @see EventBeforeSave
584
	 * @return boolean
585
	 */
586 114
	private function _beforeSave($model, $event = null)
587
	{
588 114
		$result = Event::Valid($model, EntityManagerInterface::EventBeforeSave);
589 114
		if ($result)
590
		{
591 111
			if (!empty($event))
592
			{
593 42
				Event::trigger($model, $event);
594
			}
595 111
			(new Signal)->emit(new BeforeSave($model));
596
		}
597 114
		return $result;
598
	}
599
600
	/**
601
	 * Take care of EventAfterSave
602
	 * @see EventAfterSave
603
	 */
604 111
	private function _afterSave($model, $event = null)
605
	{
606 111
		Event::trigger($model, EntityManagerInterface::EventAfterSave);
607 111
		if (!empty($event))
608
		{
609 41
			Event::trigger($model, $event);
610
		}
611 111
		(new Signal)->emit(new AfterSave($model));
612 111
		ScenarioManager::setScenario($model, ScenariosInterface::Update);
613 111
	}
614
615
	/**
616
	 * This method is invoked before deleting a record.
617
	 * The default implementation raises the {@link onBeforeDelete} event.
618
	 * You may override this method to do any preparation work for record deletion.
619
	 * Make sure you call the parent implementation so that the event is raised properly.
620
	 * @return boolean whether the record should be deleted. Defaults to true.
621
	 * @since v1.0
622
	 */
623 14
	private function _beforeDelete()
624
	{
625 14
		$result = Event::valid($this->model, EntityManagerInterface::EventBeforeDelete);
626 14
		if ($result)
627
		{
628 13
			(new Signal)->emit(new BeforeDelete($this->model));
629 13
			ScenarioManager::setScenario($this->model, ScenariosInterface::Delete);
630
		}
631 14
		return $result;
632
	}
633
634
	/**
635
	 * This method is invoked after deleting a record.
636
	 * The default implementation raises the {@link onAfterDelete} event.
637
	 * You may override this method to do postprocessing after the record is deleted.
638
	 * Make sure you call the parent implementation so that the event is raised properly.
639
	 * @since v1.0
640
	 */
641 9
	private function _afterDelete()
642
	{
643 9
		$event = new ModelEvent($this->model);
644 9
		Event::trigger($this->model, EntityManagerInterface::EventAfterDelete, $event);
645 9
		(new Signal)->emit(new AfterDelete($this->model));
646 9
	}
647
648
// </editor-fold>
649
}
650