Test Failed
Push — master ( aaf2c8...6e983e )
by Adam
06:42
created

BlameableSubscriber::getSubscribedEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * BlameableSubscriber.php
4
 *
5
 * @copyright      More in license.md
6
 * @license        https://www.ipublikuj.eu
7
 * @author         Adam Kadlec <[email protected]>
8
 * @package        iPublikuj:DoctrineBlameable!
9
 * @subpackage     Events
10
 * @since          1.0.0
11
 *
12
 * @date           01.01.16
13
 */
14
15
declare(strict_types = 1);
16
17
namespace IPub\DoctrineBlameable\Events;
18
19
use Nette;
20
21
use Doctrine\Common;
22
use Doctrine\ORM;
23
24
use IPub\DoctrineBlameable\Exceptions;
25
use IPub\DoctrineBlameable\Mapping;
26
27
/**
28
 * Doctrine blameable subscriber
29
 *
30
 * @package        iPublikuj:DoctrineBlameable!
31
 * @subpackage     Events
32
 *
33
 * @author         Adam Kadlec <[email protected]>
34
 */
35 1
final class BlameableSubscriber implements Common\EventSubscriber
36
{
37
	/**
38
	 * Implement nette smart magic
39
	 */
40 1
	use Nette\SmartObject;
41
42
	/**
43
	 * @var callable
44
	 */
45
	private $userCallable;
46
47
	/**
48
	 * @var mixed
49
	 */
50
	private $user;
51
52
	/**
53
	 * @var Mapping\Driver\Blameable
54
	 */
55
	private $driver;
56
57
	/**
58
	 * Register events
59
	 *
60
	 * @return string[]
61
	 */
62
	public function getSubscribedEvents() : array
63
	{
64
		return [
65
			ORM\Events::loadClassMetadata,
66
			ORM\Events::onFlush,
67
		];
68
	}
69
70
	/**
71
	 * @param callable|NULL $userCallable
72
	 * @param Mapping\Driver\Blameable $driver
73
	 */
74
	public function __construct(
75
		$userCallable = NULL,
76
		Mapping\Driver\Blameable $driver
77
	) {
78
		$this->driver = $driver;
79
		$this->userCallable = $userCallable;
80
	}
81
82
	/**
83
	 * @param ORM\Event\LoadClassMetadataEventArgs $eventArgs
84
	 *
85
	 * @return void
86
	 *
87
	 * @throws ORM\Mapping\MappingException
88
	 */
89
	public function loadClassMetadata(ORM\Event\LoadClassMetadataEventArgs $eventArgs) : void
90
	{
91
		/** @var ORM\Mapping\ClassMetadata $classMetadata */
92
		$classMetadata = $eventArgs->getClassMetadata();
93
		$this->driver->loadMetadataForObjectClass($eventArgs->getObjectManager(), $classMetadata);
94
95
		// Register pre persist event
96
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
97
		// Register pre update event
98
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
99
		// Register pre remove event
100
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
101
	}
102
103
	/**
104
	 * @param ORM\Event\OnFlushEventArgs $eventArgs
105
	 *
106
	 * @return void
107
	 *
108
	 * @throws ORM\Mapping\MappingException
109
	 */
110
	public function onFlush(ORM\Event\OnFlushEventArgs $eventArgs) : void
111
	{
112
		$em = $eventArgs->getEntityManager();
113
		$uow = $em->getUnitOfWork();
114
115
		// Check all scheduled updates
116
		foreach ($uow->getScheduledEntityUpdates() as $object) {
117
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
118
			$classMetadata = $em->getClassMetadata(get_class($object));
119
120
			if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
121
				$changeSet = $uow->getEntityChangeSet($object);
122
				$needChanges = FALSE;
123
124
				if ($uow->isScheduledForInsert($object) && isset($config['create'])) {
125
					foreach ($config['create'] as $field) {
126
						// Field can not exist in change set, when persisting embedded document without parent for example
127
						$new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : FALSE;
128
129
						if ($new === NULL) { // let manual values
130
							$needChanges = TRUE;
131
							$this->updateField($uow, $object, $classMetadata, $field);
132
						}
133
					}
134
				}
135
136 View Code Duplication
				if (isset($config['update'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
137
					foreach ($config['update'] as $field) {
138
						$isInsertAndNull = $uow->isScheduledForInsert($object)
139
							&& array_key_exists($field, $changeSet)
140
							&& $changeSet[$field][1] === NULL;
141
142
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
143
							$needChanges = TRUE;
144
							$this->updateField($uow, $object, $classMetadata, $field);
145
						}
146
					}
147
				}
148
149 View Code Duplication
				if (isset($config['delete'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
150
					foreach ($config['delete'] as $field) {
151
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
152
							&& array_key_exists($field, $changeSet)
153
							&& $changeSet[$field][1] === NULL;
154
155
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
156
							$needChanges = TRUE;
157
							$this->updateField($uow, $object, $classMetadata, $field);
158
						}
159
					}
160
				}
161
162
				if (isset($config['change'])) {
163
					foreach ($config['change'] as $options) {
164
						if (isset($changeSet[$options['field']])) {
165
							continue; // Value was set manually
166
						}
167
168
						if (!is_array($options['trackedField'])) {
169
							$singleField = TRUE;
170
							$trackedFields = [$options['trackedField']];
171
172
						} else {
173
							$singleField = FALSE;
174
							$trackedFields = $options['trackedField'];
175
						}
176
177
						foreach ($trackedFields as $tracked) {
178
							$trackedChild = NULL;
179
							$parts = explode('.', $tracked);
180
181
							if (isset($parts[1])) {
182
								$tracked = $parts[0];
183
								$trackedChild = $parts[1];
184
							}
185
186
							if (isset($changeSet[$tracked])) {
187
								$changes = $changeSet[$tracked];
188
189
								if (isset($trackedChild)) {
190
									$changingObject = $changes[1];
191
192
									if (!is_object($changingObject)) {
193
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
194
									}
195
196
									/** @var ORM\Mapping\ClassMetadata $objectMeta */
197
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
198
									$em->initializeObject($changingObject);
199
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
200
201
								} else {
202
									$value = $changes[1];
203
								}
204
205
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
206
									$needChanges = TRUE;
207
									$this->updateField($uow, $object, $classMetadata, $options['field']);
208
								}
209
							}
210
						}
211
					}
212
				}
213
214
				if ($needChanges) {
215
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
216
				}
217
			}
218
		}
219
	}
220
221
	/**
222
	 * @param mixed $entity
223
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
224
	 *
225
	 * @return void
226
	 *
227
	 * @throws ORM\Mapping\MappingException
228
	 */
229
	public function prePersist($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
230
	{
231
		$em = $eventArgs->getEntityManager();
232
		$uow = $em->getUnitOfWork();
233
		$classMetadata = $em->getClassMetadata(get_class($entity));
234
235
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
236
			foreach(['update', 'create'] as $event) {
237
				if (isset($config[$event])) {
238
					$this->updateFields($config[$event], $uow, $entity, $classMetadata);
239
				}
240
			}
241
		}
242
	}
243
244
	/**
245
	 * @param mixed $entity
246
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
247
	 *
248
	 * @return void
249
	 *
250
	 * @throws ORM\Mapping\MappingException
251
	 */
252 View Code Duplication
	public function preUpdate($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
	{
254
		$em = $eventArgs->getEntityManager();
255
		$uow = $em->getUnitOfWork();
256
		$classMetadata = $em->getClassMetadata(get_class($entity));
257
258
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
259
			if (isset($config['update'])) {
260
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
261
			}
262
		}
263
	}
264
265
	/**
266
	 * @param mixed $entity
267
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
268
	 *
269
	 * @return void
270
	 *
271
	 * @throws ORM\Mapping\MappingException
272
	 */
273 View Code Duplication
	public function preRemove($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
	{
275
		$em = $eventArgs->getEntityManager();
276
		$uow = $em->getUnitOfWork();
277
		$classMetadata = $em->getClassMetadata(get_class($entity));
278
279
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
280
			if (isset($config['delete'])) {
281
				$this->updateFields($config['delete'], $uow, $entity, $classMetadata);
282
			}
283
		}
284
	}
285
286
	/**
287
	 * Set a custom representation of current user
288
	 *
289
	 * @param mixed $user
290
	 *
291
	 * @return void
292
	 */
293
	public function setUser($user) : void
294
	{
295
		$this->user = $user;
296
	}
297
298
	/**
299
	 * Get current user, either if $this->user is present or from userCallable
300
	 *
301
	 * @return mixed The user representation
302
	 */
303
	public function getUser()
304
	{
305
		if ($this->user !== NULL) {
306
			return $this->user;
307
		}
308
309
		if ($this->userCallable === NULL) {
310
			return NULL;
311
		}
312
313
		$callable = $this->userCallable;
314
315
		return $callable();
316
	}
317
318
	/**
319
	 * @param callable $callable
320
	 *
321
	 * @return void
322
	 */
323
	public function setUserCallable(callable $callable) : void
324
	{
325
		$this->userCallable = $callable;
326
	}
327
328
	/**
329
	 * @param array $fields
330
	 * @param ORM\UnitOfWork $uow
331
	 * @param mixed $object
332
	 * @param ORM\Mapping\ClassMetadata $classMetadata
333
	 *
334
	 * @return void
335
	 */
336
	private function updateFields(array $fields, ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata) : void
337
	{
338
		foreach ($fields as $field) {
339
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
340
				$this->updateField($uow, $object, $classMetadata, $field);
341
			}
342
		}
343
	}
344
345
	/**
346
	 * Updates a field
347
	 *
348
	 * @param ORM\UnitOfWork $uow
349
	 * @param mixed $object
350
	 * @param ORM\Mapping\ClassMetadata $classMetadata
351
	 * @param string $field
352
	 *
353
	 * @return void
354
	 */
355
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, string $field) : void
356
	{
357
		$property = $classMetadata->getReflectionProperty($field);
358
359
		$oldValue = $property->getValue($object);
360
		$newValue = $this->getUserValue($classMetadata, $field);
361
362
		$property->setValue($object, $newValue);
363
364
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
365
		$uow->scheduleExtraUpdate($object, [
366
			$field => [$oldValue, $newValue],
367
		]);
368
	}
369
370
	/**
371
	 * Get the user value to set on a blameable field
372
	 *
373
	 * @param ORM\Mapping\ClassMetadata $classMetadata
374
	 * @param string $field
375
	 *
376
	 * @return mixed
377
	 */
378
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, string $field)
379
	{
380
		$user = $this->getUser();
381
382
		if ($classMetadata->hasAssociation($field)) {
383
			if ($user !== NULL && ! is_object($user)) {
384
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
385
			}
386
387
			return $user;
388
		}
389
390
		// Ok so its not an association, then it is a string
391
		if (is_object($user)) {
392
			if (method_exists($user, '__toString')) {
393
				return $user->__toString();
394
			}
395
396
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
397
		}
398
399
		return $user;
400
	}
401
402
	/**
403
	 * @param ORM\Mapping\ClassMetadata $classMetadata
404
	 * @param string $eventName
405
	 *
406
	 * @return void
407
	 *
408
	 * @throws ORM\Mapping\MappingException
409
	 */
410
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, string $eventName) : void
411
	{
412
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
413
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
414
		}
415
	}
416
417
	/**
418
	 * @param ORM\Mapping\ClassMetadata $classMetadata
419
	 * @param string $eventName
420
	 * @param string $listenerClass
421
	 *
422
	 * @return bool
423
	 */
424
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, string $eventName, string $listenerClass) : bool 
425
	{
426
		if (!isset($classMetadata->entityListeners[$eventName])) {
427
			return FALSE;
428
		}
429
430
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
431
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
432
				return TRUE;
433
			}
434
		}
435
436
		return FALSE;
437
	}
438
}
439