Completed
Push — master ( 28f29f...4081af )
by Adam
08:00 queued 05:40
created

BlameableSubscriber::getUserValue()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.0359

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 9
cts 10
cp 0.9
rs 8.5906
c 0
b 0
f 0
cc 6
eloc 11
nc 5
nop 2
crap 6.0359
1
<?php
2
/**
3
 * BlameableSubscriber.php
4
 *
5
 * @copyright      More in license.md
6
 * @license        http://www.ipublikuj.eu
7
 * @author         Adam Kadlec http://www.ipublikuj.eu
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
use Nette\Utils;
21
22
use Doctrine;
23
use Doctrine\Common;
24
use Doctrine\ORM;
25
26
use IPub;
27
use IPub\DoctrineBlameable;
28
use IPub\DoctrineBlameable\Exceptions;
29
use IPub\DoctrineBlameable\Mapping;
30
31
/**
32
 * Doctrine blameable subscriber
33
 *
34
 * @package        iPublikuj:DoctrineBlameable!
35
 * @subpackage     Events
36
 *
37
 * @author         Adam Kadlec <[email protected]>
38
 */
39 1
final class BlameableSubscriber extends Nette\Object implements Common\EventSubscriber
40
{
41
	/**
42
	 * @var callable
43
	 */
44
	private $userCallable;
45
46
	/**
47
	 * @var mixed
48
	 */
49
	private $user;
50
51
	/**
52
	 * @var Mapping\Driver\Blameable
53
	 */
54
	private $driver;
55
56
	/**
57
	 * Register events
58
	 *
59
	 * @return array
60
	 */
61
	public function getSubscribedEvents() : array
62
	{
63
		return [
64 1
			ORM\Events::loadClassMetadata,
65 1
			ORM\Events::onFlush,
66
		];
67
	}
68
69
	/**
70
	 * @param callable|NULL $userCallable
71
	 * @param Mapping\Driver\Blameable $driver
72
	 */
73
	public function __construct(
74
		$userCallable = NULL,
75
		Mapping\Driver\Blameable $driver
76
	) {
77 1
		$this->driver = $driver;
78 1
		$this->userCallable = $userCallable;
79 1
	}
80
81
	/**
82
	 * @param ORM\Event\LoadClassMetadataEventArgs $eventArgs
83
	 *
84
	 * @return void
85
	 *
86
	 * @throws Exceptions\InvalidMappingException
87
	 */
88
	public function loadClassMetadata(ORM\Event\LoadClassMetadataEventArgs $eventArgs)
89
	{
90
		/** @var ORM\Mapping\ClassMetadata $classMetadata */
91 1
		$classMetadata = $eventArgs->getClassMetadata();
92 1
		$this->driver->loadMetadataForObjectClass($eventArgs->getObjectManager(), $classMetadata);
93
94
		// Register pre persist event
95 1
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
96
		// Register pre update event
97 1
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
98
		// Register pre remove event
99 1
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
100 1
	}
101
102
	/**
103
	 * @param ORM\Event\OnFlushEventArgs $eventArgs
104
	 *
105
	 * @return void
106
	 *
107
	 * @throws Exceptions\UnexpectedValueException
108
	 */
109
	public function onFlush(ORM\Event\OnFlushEventArgs $eventArgs)
110
	{
111 1
		$em = $eventArgs->getEntityManager();
112 1
		$uow = $em->getUnitOfWork();
113
114
		// Check all scheduled updates
115 1
		foreach ($uow->getScheduledEntityUpdates() as $object) {
116
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
117 1
			$classMetadata = $em->getClassMetadata(get_class($object));
118
119 1
			if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
120 1
				$changeSet = $uow->getEntityChangeSet($object);
121 1
				$needChanges = FALSE;
122
123 1
				if ($uow->isScheduledForInsert($object) && isset($config['create'])) {
124
					foreach ($config['create'] as $field) {
125
						// Field can not exist in change set, when persisting embedded document without parent for example
126
						$new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : FALSE;
127
128
						if ($new === NULL) { // let manual values
129
							$needChanges = TRUE;
130
							$this->updateField($uow, $object, $classMetadata, $field);
131
						}
132
					}
133
				}
134
135 1 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...
136 1
					foreach ($config['update'] as $field) {
137 1
						$isInsertAndNull = $uow->isScheduledForInsert($object)
138 1
							&& array_key_exists($field, $changeSet)
139 1
							&& $changeSet[$field][1] === NULL;
140
141 1
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
142 1
							$needChanges = TRUE;
143 1
							$this->updateField($uow, $object, $classMetadata, $field);
144
						}
145
					}
146
				}
147
148 1 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...
149 1
					foreach ($config['delete'] as $field) {
150 1
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
151 1
							&& array_key_exists($field, $changeSet)
152 1
							&& $changeSet[$field][1] === NULL;
153
154 1
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
155 1
							$needChanges = TRUE;
156 1
							$this->updateField($uow, $object, $classMetadata, $field);
157
						}
158
					}
159
				}
160
161 1
				if (isset($config['change'])) {
162 1
					foreach ($config['change'] as $options) {
163 1
						if (isset($changeSet[$options['field']])) {
164 1
							continue; // Value was set manually
165
						}
166
167 1
						if (!is_array($options['trackedField'])) {
168 1
							$singleField = TRUE;
169 1
							$trackedFields = [$options['trackedField']];
170
171
						} else {
172
							$singleField = FALSE;
173
							$trackedFields = $options['trackedField'];
174
						}
175
176 1
						foreach ($trackedFields as $tracked) {
177 1
							$trackedChild = NULL;
178 1
							$parts = explode('.', $tracked);
179
180 1
							if (isset($parts[1])) {
181 1
								$tracked = $parts[0];
182 1
								$trackedChild = $parts[1];
183
							}
184
185 1
							if (isset($changeSet[$tracked])) {
186 1
								$changes = $changeSet[$tracked];
187
188 1
								if (isset($trackedChild)) {
189 1
									$changingObject = $changes[1];
190
191 1
									if (!is_object($changingObject)) {
192
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
193
									}
194
195
									/** @var ORM\Mapping\ClassMetadata $objectMeta */
196 1
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
197 1
									$em->initializeObject($changingObject);
198 1
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
199
200
								} else {
201
									$value = $changes[1];
202
								}
203
204 1
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
205 1
									$needChanges = TRUE;
206 1
									$this->updateField($uow, $object, $classMetadata, $options['field']);
207
								}
208
							}
209
						}
210
					}
211
				}
212
213 1
				if ($needChanges) {
214 1
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
215
				}
216
			}
217
		}
218 1
	}
219
220
	/**
221
	 * @param mixed $entity
222
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
223
	 *
224
	 * @return void
225
	 */
226
	public function prePersist($entity, ORM\Event\LifecycleEventArgs $eventArgs)
227
	{
228 1
		$em = $eventArgs->getEntityManager();
229 1
		$uow = $em->getUnitOfWork();
230 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
231
232 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
233 1
			foreach(['update', 'create'] as $event) {
234 1
				if (isset($config[$event])) {
235 1
					$this->updateFields($config[$event], $uow, $entity, $classMetadata);
236
				}
237
			}
238
		}
239 1
	}
240
241
	/**
242
	 * @param mixed $entity
243
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
244
	 *
245
	 * @return void
246
	 */
247 View Code Duplication
	public function preUpdate($entity, ORM\Event\LifecycleEventArgs $eventArgs)
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...
248
	{
249 1
		$em = $eventArgs->getEntityManager();
250 1
		$uow = $em->getUnitOfWork();
251 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
252
253 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
254 1
			if (isset($config['update'])) {
255 1
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
256
			}
257
		}
258 1
	}
259
260
	/**
261
	 * @param mixed $entity
262
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
263
	 *
264
	 * @return void
265
	 */
266 View Code Duplication
	public function preRemove($entity, ORM\Event\LifecycleEventArgs $eventArgs)
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...
267
	{
268 1
		$em = $eventArgs->getEntityManager();
269 1
		$uow = $em->getUnitOfWork();
270 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
271
272 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
273 1
			if (isset($config['delete'])) {
274 1
				$this->updateFields($config['delete'], $uow, $entity, $classMetadata);
275
			}
276
		}
277 1
	}
278
279
	/**
280
	 * Set a custom representation of current user
281
	 *
282
	 * @param mixed $user
283
	 *
284
	 * @return void
285
	 */
286
	public function setUser($user)
287
	{
288 1
		$this->user = $user;
289 1
	}
290
291
	/**
292
	 * Get current user, either if $this->user is present or from userCallable
293
	 *
294
	 * @return mixed The user representation
295
	 */
296
	public function getUser()
297
	{
298 1
		if ($this->user !== NULL) {
299 1
			return $this->user;
300
		}
301
302 1
		if ($this->userCallable === NULL) {
303
			return NULL;
304
		}
305
306 1
		$callable = $this->userCallable;
307
308 1
		return $callable();
309
	}
310
311
	/**
312
	 * @param callable $callable
313
	 *
314
	 * @return void
315
	 */
316
	public function setUserCallable(callable $callable)
317
	{
318 1
		$this->userCallable = $callable;
319 1
	}
320
321
	/**
322
	 * @param array $fields
323
	 * @param ORM\UnitOfWork $uow
324
	 * @param mixed $object
325
	 * @param ORM\Mapping\ClassMetadata $classMetadata
326
	 *
327
	 * @return void
328
	 */
329
	private function updateFields(array $fields, ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata)
330
	{
331 1
		foreach ($fields as $field) {
332 1
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
333 1
				$this->updateField($uow, $object, $classMetadata, $field);
334
			}
335
		}
336 1
	}
337
338
	/**
339
	 * Updates a field
340
	 *
341
	 * @param ORM\UnitOfWork $uow
342
	 * @param mixed $object
343
	 * @param ORM\Mapping\ClassMetadata $classMetadata
344
	 * @param string $field
345
	 *
346
	 * @return void
347
	 */
348
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, string $field)
349
	{
350 1
		$property = $classMetadata->getReflectionProperty($field);
351
352 1
		$oldValue = $property->getValue($object);
353 1
		$newValue = $this->getUserValue($classMetadata, $field);
354
355 1
		$property->setValue($object, $newValue);
356
357 1
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
358 1
		$uow->scheduleExtraUpdate($object, [
359 1
			$field => [$oldValue, $newValue],
360
		]);
361 1
	}
362
363
	/**
364
	 * Get the user value to set on a blameable field
365
	 *
366
	 * @param ORM\Mapping\ClassMetadata $classMetadata
367
	 * @param string $field
368
	 *
369
	 * @return mixed
370
	 */
371
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, string $field)
372
	{
373 1
		$user = $this->getUser();
374
375 1
		if ($classMetadata->hasAssociation($field)) {
376 1
			if ($user !== NULL && ! is_object($user)) {
377 1
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
378
			}
379
380 1
			return $user;
381
		}
382
383
		// Ok so its not an association, then it is a string
384 1
		if (is_object($user)) {
385 1
			if (method_exists($user, '__toString')) {
386
				return $user->__toString();
387
			}
388
389 1
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
390
		}
391
392 1
		return $user;
393
	}
394
395
	/**
396
	 * @param ORM\Mapping\ClassMetadata $classMetadata
397
	 * @param string $eventName
398
	 *
399
	 * @return void
400
	 *
401
	 * @throws ORM\Mapping\MappingException
402
	 */
403
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, string $eventName)
404
	{
405 1
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
406 1
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
407
		}
408 1
	}
409
410
	/**
411
	 * @param ORM\Mapping\ClassMetadata $classMetadata
412
	 * @param string $eventName
413
	 * @param string $listenerClass
414
	 *
415
	 * @return bool
416
	 */
417
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, string $eventName, string $listenerClass) : bool 
418
	{
419 1
		if (!isset($classMetadata->entityListeners[$eventName])) {
420 1
			return FALSE;
421
		}
422
423 1
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
424 1
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
425 1
				return TRUE;
426
			}
427
		}
428
429
		return FALSE;
430
	}
431
}
432