Completed
Push — master ( 898d30...0483c4 )
by Peter
21:25
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 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4286
cc 2
eloc 3
nc 2
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 http://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\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
use MongoException;
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
	 * @throws ManganException
98
	 */
99 71
	public function __construct(AnnotatedInterface $model)
100
	{
101 71
		$this->model = $model;
102 71
		$this->sm = new ScopeManager($model);
103 71
		$this->options = new EntityOptions($model);
104 68
		$this->collectionName = CollectionNamer::nameCollection($model);
105 68
		$this->meta = ManganMeta::create($model);
106 68
		$this->validator = new Validator($model);
107 68
		$mangan = Mangan::fromModel($model);
108 68
		if (!$this->collectionName)
109 68
		{
110
			throw new ManganException(sprintf('Invalid collection name for model: `%s`', $this->meta->type()->name));
111
		}
112 68
		$this->_collection = new MongoCollection($mangan->getDbInstance(), $this->collectionName);
113 68
	}
114
115
	/**
116
	 * Create model related entity manager.
117
	 * This will create customized entity manger if defined in model with EntityManager annotation.
118
	 * If no custom entity manager is defined this will return default EntityManager.
119
	 * @param AnnotatedInterface $model
120
	 * @return EntityManagerInterface
121
	 */
122 14
	public static function create($model)
123
	{
124 14
		$emClass = ManganMeta::create($model)->type()->entityManager ? : EntityManager::class;
125 14
		return new $emClass($model);
126
	}
127
128
	/**
129
	 * Set attributes en masse.
130
	 * Attributes will be filtered according to SafeAnnotation.
131
	 * Only attributes marked as safe will be set, other will be ignored.
132
	 *
133
	 * @param mixed[] $atributes
134
	 */
135
	public function setAttributes($atributes)
136
	{
137
		SafeArray::toModel($atributes, $this->model, $this->model);
138
	}
139
140
	/**
141
	 * Inserts a row into the table based on this active record attributes.
142
	 * If the table's primary key is auto-incremental and is null before insertion,
143
	 * it will be populated with the actual value after insertion.
144
	 *
145
	 * Note, validation is not performed in this method. You may call {@link validate} to perform the validation.
146
	 * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false,
147
	 * and its {@link scenario} property will be set to be 'update'.
148
	 *
149
	 * @param AnnotatedInterface $model if want to insert different model than set in constructor
150
	 *
151
	 * @return boolean whether the attributes are valid and the record is inserted successfully.
152
	 * @throws MongoException if the record is not new
153
	 * @throws MongoException on fail of insert or insert of empty document
154
	 * @throws MongoException on fail of insert, when safe flag is set to true
155
	 * @throws MongoException on timeout of db operation , when safe flag is set to true
156
	 * @since v1.0
157
	 */
158 28
	public function insert(AnnotatedInterface $model = null)
159
	{
160 28
		$model = $model ? : $this->model;
161 28
		if ($this->_beforeSave($model, EntityManagerInterface::EventBeforeInsert))
162 27
		{
163 27
			$rawData = RawArray::fromModel($model);
164 5
			$rawResult = $this->_collection->insert($rawData, $this->options->getSaveOptions());
165 5
			$result = $this->_result($rawResult, true);
166
167
			if ($result)
168 5
			{
169 5
				$this->_afterSave($model, EntityManagerInterface::EventAfterInsert);
170 5
				return true;
171
			}
172
			throw new MongoException('Can\t save the document to disk, or attempting to save an empty document.');
173
		}
174
		return false;
175
	}
176
177
	/**
178
	 * Updates the row represented by this active document.
179
	 * All loaded attributes will be saved to the database.
180
	 * Note, validation is not performed in this method. You may call {@link validate} to perform the validation.
181
	 *
182
	 * @param array $attributes list of attributes that need to be saved. Defaults to null,
183
	 * meaning all attributes that are loaded from DB will be saved.
184
185
	 * @return boolean whether the update is successful
186
	 * @throws MongoException if the record is new
187
	 * @throws MongoException on fail of update
188
	 * @throws MongoException on timeout of db operation , when safe flag is set to true
189
	 * @since v1.0
190
	 */
191
	public function update(array $attributes = null)
192
	{
193
		if ($this->_beforeSave($this->model, EntityManagerInterface::EventBeforeUpdate))
194
		{
195
			$rawData = RawArray::fromModel($this->model);
196
197
			// filter attributes if set in param
198
			$modify = false;
199
			if ($attributes !== null)
200
			{
201
				$modify = true;
202
				foreach ($rawData as $key => $value)
203
				{
204
					if (!in_array($key, $attributes))
205
					{
206
						unset($rawData[$key]);
207
					}
208
				}
209
			}
210
			if ($modify)
211
			{
212
				$criteria = PkManager::prepareFromModel($this->model);
213
				$result = $this->getCollection()->update($criteria->getConditions(), ['$set' => $rawData], $this->options->getSaveOptions(['multiple' => false]));
214
			}
215
			else
216
			{
217
				$result = $this->getCollection()->save($rawData, $this->options->getSaveOptions());
218
			}
219
			$result = $this->_result($result);
220
			if ($result)
221
			{
222
				$this->_afterSave($this->model, EntityManagerInterface::EventAfterUpdate);
223
				return true;
224
			}
225
			throw new MongoException('Can\t save the document to disk, or attempting to save an empty document.');
226
		}
227
		return false;
228
	}
229
230
	/**
231
	 * Atomic, in-place update method.
232
	 *
233
	 * @since v1.3.6
234
	 * @param Modifier $modifier updating rules to apply
235
	 * @param CriteriaInterface $criteria condition to limit updating rules
236
	 * @return boolean
237
	 */
238
	public function updateAll(Modifier $modifier, CriteriaInterface $criteria = null)
239
	{
240
		if ($modifier->canApply())
241
		{
242
			$criteria = $this->sm->apply($criteria);
243
			$result = $this->getCollection()->update($criteria->getConditions(), $modifier->getModifiers(), $this->options->getSaveOptions([
244
						'upsert' => false,
245
						'multiple' => true
246
			]));
247
			return $this->_result($result);
248
		}
249
		else
250
		{
251
			return false;
252
		}
253
	}
254
255
	/**
256
	 * Saves the current record.
257
	 *
258
	 * The record is inserted as a row into the database collection or updated if exists.
259
	 *
260
	 * Validation will be performed before saving the record. If the validation fails,
261
	 * the record will not be saved. You can call {@link getErrors()} to retrieve the
262
	 * validation errors.
263
	 *
264
	 * @param boolean $runValidation whether to perform validation before saving the record.
265
	 * If the validation fails, the record will not be saved to database.
266
	 * @param AnnotatedInterface $model if want to insert different model than set in constructor
267
	 * @return boolean whether the saving succeeds
268
	 * @since v1.0
269
	 */
270 39
	public function save($runValidation = true, $model = null)
271
	{
272 39
		if (!$runValidation || $this->validator->validate())
273 39
		{
274 39
			$model = $model ? : $this->model;
275 39
			if ($this->_beforeSave($model))
276 39
			{
277 36
				$data = RawArray::fromModel($model);
278 10
				$rawResult = $this->_collection->save($data, $this->options->getSaveOptions());
279 10
				$result = $this->_result($rawResult, true);
280
281
				if ($result)
282 10
				{
283 10
					$this->_afterSave($model);
284 10
					return true;
285
				}
286
				throw new MongoException("Can't save the document to disk, or attempting to save an empty document");
287
			}
288 3
			return false;
289
		}
290
		else
291
		{
292 2
			return false;
293
		}
294
	}
295
296
	/**
297
	 * Reloads document from database.
298
	 * It return true if document is reloaded and false if it's no longer exists.
299
	 *
300
	 * @return boolean
301
	 */
302 1
	public function refresh()
303
	{
304 1
		$conditions = PkManager::prepareFromModel($this->model)->getConditions();
305 1
		$data = $this->getCollection()->findOne($conditions);
306 1
		if (null !== $data)
307 1
		{
308 1
			RawArray::toModel($data, $this->model, $this->model);
309 1
			return true;
310
		}
311
		else
312
		{
313
			return false;
314
		}
315
	}
316
317
	/**
318
	 * Deletes the document from database.
319
	 * @return boolean whether the deletion is successful.
320
	 * @throws MongoException if the record is new
321
	 */
322
	public function delete()
323
	{
324
		if ($this->_beforeDelete())
325
		{
326
			$result = $this->deleteOne(PkManager::prepareFromModel($this->model));
327
328
			if ($result !== false)
329
			{
330
				$this->_afterDelete();
331
				return true;
332
			}
333
			else
334
			{
335
				return false;
336
			}
337
		}
338
		else
339
		{
340
			return false;
341
		}
342
	}
343
344
	/**
345
	 * Deletes one document with the specified primary keys.
346
	 * <b>Does not raise beforeDelete</b>
347
	 * See {@link find()} for detailed explanation about $condition and $params.
348
	 * @param array|CriteriaInterface $criteria query criteria.
349
	 * @since v1.0
350
	 */
351
	public function deleteOne($criteria = null)
352
	{
353
		$criteria = $this->sm->apply($criteria);
354
355
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
356
					'justOne' => true
357
		]));
358
		return $this->_result($result);
359
	}
360
361
	/**
362
	 * Deletes document with the specified primary key.
363
	 * See {@link find()} for detailed explanation about $condition and $params.
364
	 * @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).
365
	 * @param array|CriteriaInterface $criteria query criteria.
366
	 * @since v1.0
367
	 */
368
	public function deleteByPk($pkValue, $criteria = null)
369
	{
370
		if ($this->_beforeDelete())
371
		{
372
			$criteria = $this->sm->apply($criteria);
373
			$criteria->mergeWith(PkManager::prepare($this->model, $pkValue));
374
375
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
376
						'justOne' => true
377
			]));
378
			return $this->_result($result);
379
		}
380
		return false;
381
	}
382
383
	/**
384
	 * Deletes documents with the specified primary keys.
385
	 * See {@link find()} for detailed explanation about $condition and $params.
386
	 * @param mixed[] $pkValues Primary keys array
387
	 * @param array|CriteriaInterface $criteria query criteria.
388
	 * @since v1.0
389
	 */
390
	public function deleteAllByPk($pkValues, $criteria = null)
391
	{
392
		if ($this->_beforeDelete())
393
		{
394
			$criteria = $this->sm->apply($criteria);
395
			$criteria->mergeWith(PkManager::prepareAll($this->model, $pkValues, $criteria));
396
			$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
397
						'justOne' => false
398
			]));
399
			return $this->_result($result);
400
		}
401
		return false;
402
	}
403
404
	/**
405
	 * Deletes documents with the specified primary keys.
406
	 * <b>Does not raise beforeDelete</b>
407
	 * See {@link find()} for detailed explanation about $condition and $params.
408
	 * @param array|CriteriaInterface $criteria query criteria.
409
	 * @since v1.0
410
	 */
411
	public function deleteAll($criteria = null)
412
	{
413
		$criteria = $this->sm->apply($criteria);
414
415
		$result = $this->getCollection()->remove($criteria->getConditions(), $this->options->getSaveOptions([
416
					'justOne' => false
417
		]));
418
		return $this->_result($result);
419
	}
420
421 15
	public function getCollection()
422
	{
423 15
		return $this->_collection;
424
	}
425
426
	/**
427
	 * Make status uniform
428
	 * @param bool|array $result
429
	 * @param bool $insert Set to true for inserts
430
	 * @return bool Return true if secceed
431
	 */
432 15
	private function _result($result, $insert = false)
433
	{
434 15
		if (is_array($result))
435 15
		{
436
			if ($insert)
437 15
			{
438 15
				return (bool) $result['ok'];
439
			}
440
			return (bool) $result['n'];
441
		}
442
		return $result;
443
	}
444
445
// <editor-fold defaultstate="collapsed" desc="Event and Signal handling">
446
447
	/**
448
	 * Take care of EventBeforeSave
449
	 * @see EventBeforeSave
450
	 * @return boolean
451
	 */
452 67
	private function _beforeSave($model, $event = null)
453
	{
454 67
		$result = Event::Valid($model, EntityManagerInterface::EventBeforeSave);
455
		if ($result)
456 66
		{
457 63
			if (!empty($event))
458 63
			{
459 27
				Event::trigger($model, $event);
460 27
			}
461 63
			(new Signal)->emit(new BeforeSave($model));
462 63
		}
463 66
		return $result;
464
	}
465
466
	/**
467
	 * Take care of EventAfterSave
468
	 * @see EventAfterSave
469
	 */
470 15
	private function _afterSave($model, $event = null)
471
	{
472 15
		Event::trigger($model, EntityManagerInterface::EventAfterSave);
473 15
		if (!empty($event))
474 15
		{
475 5
			Event::trigger($model, $event);
476 5
		}
477 15
		(new Signal)->emit(new AfterSave($model));
478 15
		ScenarioManager::setScenario($model, ScenariosInterface::Update);
479 15
	}
480
481
	/**
482
	 * This method is invoked before deleting a record.
483
	 * The default implementation raises the {@link onBeforeDelete} event.
484
	 * You may override this method to do any preparation work for record deletion.
485
	 * Make sure you call the parent implementation so that the event is raised properly.
486
	 * @return boolean whether the record should be deleted. Defaults to true.
487
	 * @since v1.0
488
	 */
489 68
	private function _beforeDelete()
490
	{
491
		$result = Event::valid($this->model, EntityManagerInterface::EventBeforeDelete);
492
		if ($result)
493
		{
494
			(new Signal)->emit(new BeforeDelete($this->model));
495
			ScenarioManager::setScenario($this->model, ScenariosInterface::Delete);
496 68
		}
497
		return $result;
498
	}
499
500
	/**
501
	 * This method is invoked after deleting a record.
502
	 * The default implementation raises the {@link onAfterDelete} event.
503
	 * You may override this method to do postprocessing after the record is deleted.
504
	 * Make sure you call the parent implementation so that the event is raised properly.
505
	 * @since v1.0
506
	 */
507
	private function _afterDelete()
508
	{
509
		Event::trigger($this->model, EntityManagerInterface::EventAfterDelete, new ModelEvent($this->model));
510
		(new Signal)->emit(new AfterDelete($this->model));
511
	}
512
513
// </editor-fold>
514
}
515