Completed
Push — aspects ( 1f6fa3 )
by Peter
08:00
created

EntityManager::update()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.243

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 7
cts 10
cp 0.7
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
crap 3.243
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 122
	public function __construct(AnnotatedInterface $model, Mangan $mangan = null)
100
	{
101 122
		$this->model = $model;
102 122
		$this->sm = new ScopeManager($model);
103 122
		$this->options = new EntityOptions($model);
104 122
		$this->collectionName = CollectionNamer::nameCollection($model);
105 122
		$this->meta = ManganMeta::create($model);
106 122
		$this->validator = new Validator($model);
107 122
		if (null === $mangan)
108
		{
109 119
			$mangan = Mangan::fromModel($model);
110
		}
111 122
		if (!$this->collectionName)
112
		{
113
			throw new ManganException(sprintf('Invalid collection name for model: `%s`', $this->meta->type()->name));
114
		}
115 122
		$this->_collection = new MongoCollection($mangan->getDbInstance(), $this->collectionName);
116 122
	}
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 113
	public static function create($model, Mangan $mangan = null)
127
	{
128 113
		$emClass = ManganMeta::create($model)->type()->entityManager ?: static::class;
129 113
		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[] $attributes
138
	 */
139
	public function setAttributes($attributes)
140
	{
141
		SafeArray::toModel($attributes, $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
			throw new ManganException('Can\t save the document to disk, or attempting to save an empty document. ' . ucfirst($rawResult['errmsg']), $rawResult['code']);
182
		}
183
		AspectManager::removeAspect($model, self::AspectSaving);
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
		AspectManager::removeAspect($this->model, self::AspectSaving);
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
	 * @param array|CriteriaInterface $criteria query criteria.
227
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
228
	 * @param bool Whether tu force update/upsert document
229
	 * meaning all attributes that are loaded from DB will be saved.
230
	 * @since v1.0
231
	 */
232 85
	public function updateOne($criteria = null, array $attributes = null, $modify = false)
233
	{
234 85
		$criteria = $this->sm->apply($criteria);
235 85
		$rawData = RawArray::fromModel($this->model, $attributes);
0 ignored issues
show
Documentation introduced by
$attributes is of type null|array, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
236
237
		// filter attributes if set in param
238 85
		if ($attributes !== null)
239
		{
240 3
			$meta = ManganMeta::create($this->model);
241 3
			foreach ($attributes as $key)
242
			{
243 3
				if ($meta->$key === false)
244
				{
245 3
					throw new BadAttributeException(sprintf("Unknown attribute `%s` on model `%s`", $key, get_class($this->model)));
246
				}
247
			}
248 2
			$modify = true;
249 2
			foreach ($rawData as $key => $value)
250
			{
251 2
				if (!in_array($key, $attributes))
252
				{
253 2
					unset($rawData[$key]);
254
				}
255
			}
256
		}
257
		else
258
		{
259 85
			$fields = array_keys(ManganMeta::create($this->model)->fields());
260 85
			$setFields = array_keys($rawData);
261 85
			$diff = array_diff($fields, $setFields);
262
263 85
			if (!empty($diff))
264
			{
265 6
				$modify = true;
266
			}
267
		}
268 85
		if ($modify)
269
		{
270
			// Id could be altered, so skip it as it cannot be changed
271 8
			unset($rawData['_id']);
272 8
			$data = ['$set' => $rawData];
273
		}
274
		else
275
		{
276 82
			$data = $rawData;
277
		}
278
279 85
		$result = $this->getCollection()->update($criteria->getConditions(), $data, $this->options->getSaveOptions(['multiple' => false, 'upsert' => true]));
280 85
		return $this->_result($result);
281
	}
282
283
	/**
284
	 * Atomic, in-place update method.
285
	 *
286
	 * @since v1.3.6
287
	 * @param Modifier $modifier updating rules to apply
288
	 * @param CriteriaInterface $criteria condition to limit updating rules
289
	 * @return boolean
290
	 */
291 1
	public function updateAll(Modifier $modifier, CriteriaInterface $criteria = null)
292
	{
293 1
		if ($modifier->canApply())
294
		{
295 1
			$criteria = $this->sm->apply($criteria);
296 1
			$result = $this->getCollection()->update($criteria->getConditions(), $modifier->getModifiers(), $this->options->getSaveOptions([
297 1
						'upsert' => false,
298
						'multiple' => true
299
			]));
300 1
			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
				$msg = '';
343
				if(!empty($rawResult['errmsg']))
344
				{
345
					$msg = $rawResult['errmsg'];
346
				}
347
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document. $msg");
348
			}
349
			AspectManager::removeAspect($model, self::AspectSaving);
350
			return false;
351
		}
352
		else
353
		{
354
			AspectManager::removeAspect($this->model, self::AspectSaving);
355
			return false;
356
		}
357
	}
358
359
	/**
360
	 * Saves the current document.
361
	 *
362
	 * The record is inserted as a document into the database collection or updated if exists.
363
	 *
364
	 * Filtered out properties will remain in database - it is partial safe.
365
	 *
366
	 * Validation will be performed before saving the record. If the validation fails,
367
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
368
	 * validation errors.
369
	 *
370
	 * @param boolean $runValidation whether to perform validation before saving the record.
371
	 * If the validation fails, the record will not be saved to database.
372
	 *
373
	 * @return boolean whether the saving succeeds
374
	 * @since v1.0
375
	 */
376 82
	public function save($runValidation = true)
377
	{
378 82
		return $this->upsert($runValidation);
379
	}
380
381
	/**
382
	 * Updates or inserts the current document. This will try to update existing fields.
383
	 * Will keep already stored data if present in document.
384
	 *
385
	 * If document does not exist, a new one will be inserted.
386
	 *
387
	 * @param boolean $runValidation
388
	 * @return boolean
389
	 * @throws ManganException
390
	 */
391 86
	public function upsert($runValidation = true)
392
	{
393 86
		if (!$runValidation || $this->validator->validate())
394
		{
395 86
			$model = $this->model;
396 86
			if ($this->_beforeSave($model))
397
			{
398 83
				$criteria = PkManager::prepareFromModel($this->model);
399 83
				foreach ($criteria->getConditions() as $field => $value)
400
				{
401 83
					if (empty($this->model->$field))
402
					{
403 83
						$this->model->$field = $value;
404
					}
405
				}
406 83
				$result = $this->updateOne($criteria);
407
408 83
				if ($result)
409
				{
410 83
					$this->_afterSave($model);
411 83
					return true;
412
				}
413
				$errmsg = '';
414
				if(!empty($this->lastResult['errmsg']))
415
				{
416
					$errmsg = ucfirst($this->lastResult['errmsg']) . '.';
417
				}
418
				throw new ManganException("Can't save the document to disk, or attempting to save an empty document. $errmsg");
419
			}
420 3
			AspectManager::removeAspect($this->model, self::AspectSaving);
421 3
			return false;
422
		}
423
		else
424
		{
425 3
			AspectManager::removeAspect($this->model, self::AspectSaving);
426 3
			return false;
427
		}
428
	}
429
430
	/**
431
	 * Reloads document from database.
432
	 * It return true if document is reloaded and false if it's no longer exists.
433
	 *
434
	 * @return boolean
435
	 */
436 2
	public function refresh()
437
	{
438 2
		$conditions = PkManager::prepareFromModel($this->model)->getConditions();
439 2
		$data = $this->getCollection()->findOne($conditions);
440 2
		if (null !== $data)
441
		{
442 2
			RawArray::toModel($data, $this->model, $this->model);
443 2
			return true;
444
		}
445
		else
446
		{
447 2
			return false;
448
		}
449
	}
450
451
	/**
452
	 * Deletes the document from database.
453
	 * @return boolean whether the deletion is successful.
454
	 * @throws ManganException if the record is new
455
	 */
456 10
	public function delete()
457
	{
458 10
		if ($this->_beforeDelete())
459
		{
460 9
			$result = $this->deleteOne(PkManager::prepareFromModel($this->model));
461
462 9
			if ($result !== false)
463
			{
464 9
				$this->_afterDelete();
465 9
				return true;
466
			}
467
			else
468
			{
469 1
				return false;
470
			}
471
		}
472
		else
473
		{
474 1
			return false;
475
		}
476
	}
477
478
	/**
479
	 * Deletes one document with the specified primary keys.
480
	 * <b>Does not raise beforeDelete</b>
481
	 * See {@link find()} for detailed explanation about $condition and $params.
482
	 * @param array|CriteriaInterface $criteria query criteria.
483
	 * @since v1.0
484
	 */
485 11
	public function deleteOne($criteria = null)
486
	{
487 11
		$criteria = $this->sm->apply($criteria);
488
489 11
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
490 11
					'justOne' => true
491
		]));
492 11
		return $this->_result($result);
493
	}
494
495
	/**
496
	 * Deletes document with the specified primary key.
497
	 * See {@link find()} for detailed explanation about $condition and $params.
498
	 * @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).
499
	 * @param array|CriteriaInterface $criteria query criteria.
500
	 * @since v1.0
501
	 */
502 3
	public function deleteByPk($pkValue, $criteria = null)
503
	{
504 3
		if ($this->_beforeDelete())
505
		{
506 2
			$criteria = $this->sm->apply($criteria);
507 2
			$criteria->mergeWith(PkManager::prepare($this->model, $pkValue));
508
509 2
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
510 2
						'justOne' => true
511
			]));
512 2
			return $this->_result($result);
513
		}
514 1
		return false;
515
	}
516
517
	/**
518
	 * Deletes documents with the specified primary keys.
519
	 * See {@link find()} for detailed explanation about $condition and $params.
520
	 * @param mixed[] $pkValues Primary keys array
521
	 * @param array|CriteriaInterface $criteria query criteria.
522
	 * @since v1.0
523
	 * @return bool
524
	 */
525 3
	public function deleteAllByPk($pkValues, $criteria = null)
526
	{
527 3
		if ($this->_beforeDelete())
528
		{
529 2
			$criteria = $this->sm->apply($criteria);
530 2
			$criteria->mergeWith(PkManager::prepareAll($this->model, $pkValues, $criteria));
531 2
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
532 2
						'justOne' => false
533
			]));
534 2
			return $this->_result($result);
535
		}
536 1
		return false;
537
	}
538
539
	/**
540
	 * Deletes documents with the specified primary keys.
541
	 *
542
	 * **Does not raise beforeDelete event and does not emit signals**
543
	 *
544
	 * See {@link find()} for detailed explanation about $condition and $params.
545
	 *
546
	 * @param array|CriteriaInterface $criteria query criteria.
547
	 * @since v1.0
548
	 * @return bool
549
	 */
550 5
	public function deleteAll($criteria = null)
551
	{
552 5
		$criteria = $this->sm->apply($criteria);
553
554
		// NOTE: Do not use [justOne => false] here
555 5
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions());
556 5
		return $this->_result($result);
557
	}
558
559 117
	public function getCollection()
560
	{
561 117
		return $this->_collection;
562
	}
563
564
	/**
565
	 * Make status uniform
566
	 * @param bool|array $result
567
	 * @param bool $insert Set to true for inserts
568
	 * @return bool Return true if succeed
569
	 */
570 116
	private function _result($result, $insert = false)
571
	{
572 116
		$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...
573 116
		if (is_array($result))
574
		{
575 116
			if ($insert)
576
			{
577 36
				return (bool) $result['ok'];
578
			}
579 85
			return (bool) $result['n'];
580
		}
581
		return $result;
582
	}
583
584
// <editor-fold defaultstate="collapsed" desc="Event and Signal handling">
585
586
	/**
587
	 * Take care of EventBeforeSave
588
	 * @see EventBeforeSave
589
	 * @return boolean
590
	 */
591 118
	private function _beforeSave($model, $event = null)
592
	{
593 118
		AspectManager::addAspect($model, self::AspectSaving);
594 118
		$result = Event::Valid($model, EntityManagerInterface::EventBeforeSave);
595 118
		if ($result)
596
		{
597 115
			if (!empty($event))
598
			{
599 42
				Event::trigger($model, $event);
600
			}
601 115
			(new Signal)->emit(new BeforeSave($model));
602
		}
603 118
		return $result;
604
	}
605
606
	/**
607
	 * Take care of EventAfterSave
608
	 * @see EventAfterSave
609
	 */
610 115
	private function _afterSave($model, $event = null)
611
	{
612 115
		Event::trigger($model, EntityManagerInterface::EventAfterSave);
613 115
		if (!empty($event))
614
		{
615 41
			Event::trigger($model, $event);
616
		}
617 115
		(new Signal)->emit(new AfterSave($model));
618 115
		ScenarioManager::setScenario($model, ScenariosInterface::Update);
619 115
		AspectManager::removeAspect($model, self::AspectSaving);
620 115
	}
621
622
	/**
623
	 * This method is invoked before deleting a record.
624
	 * The default implementation raises the {@link onBeforeDelete} event.
625
	 * You may override this method to do any preparation work for record deletion.
626
	 * Make sure you call the parent implementation so that the event is raised properly.
627
	 * @return boolean whether the record should be deleted. Defaults to true.
628
	 * @since v1.0
629
	 */
630 14
	private function _beforeDelete()
631
	{
632 14
		AspectManager::addAspect($this->model, self::AspectRemoving);
633 14
		$result = Event::valid($this->model, EntityManagerInterface::EventBeforeDelete);
634 14
		if ($result)
635
		{
636 13
			(new Signal)->emit(new BeforeDelete($this->model));
637 13
			ScenarioManager::setScenario($this->model, ScenariosInterface::Delete);
638
		}
639 14
		return $result;
640
	}
641
642
	/**
643
	 * This method is invoked after deleting a record.
644
	 * The default implementation raises the {@link onAfterDelete} event.
645
	 * You may override this method to do postprocessing after the record is deleted.
646
	 * Make sure you call the parent implementation so that the event is raised properly.
647
	 * @since v1.0
648
	 */
649 9
	private function _afterDelete()
650
	{
651 9
		$event = new ModelEvent($this->model);
652 9
		Event::trigger($this->model, EntityManagerInterface::EventAfterDelete, $event);
653 9
		(new Signal)->emit(new AfterDelete($this->model));
654 9
		AspectManager::removeAspect($this->model, self::AspectRemoving);
655 9
	}
656
657
// </editor-fold>
658
}
659