Completed
Push — master ( c8f19a...ddbfd2 )
by Peter
72:09 queued 69:12
created

EntityManager   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 601
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 23

Test Coverage

Coverage 85.12%

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 23
dl 0
loc 601
ccs 183
cts 215
cp 0.8512
rs 1.5461
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 5 2
A setAttributes() 0 4 1
A save() 0 4 1
A deleteOne() 0 9 1
A deleteAll() 0 9 1
A getCollection() 0 4 1
A _afterDelete() 0 6 1
A __construct() 0 18 3
B insert() 0 24 4
A update() 0 15 3
C updateOne() 0 50 8
A updateAll() 0 16 2
B replace() 0 30 6
C upsert() 0 31 7
A refresh() 0 14 2
A delete() 0 21 3
A deleteByPk() 0 14 2
A deleteAllByPk() 0 13 2
A _result() 0 12 3
A _beforeSave() 0 13 3
A _afterSave() 0 10 2
A _beforeDelete() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like EntityManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityManager, and based on these observations, apply Extract Interface, too.

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