Passed
Push — master ( 7b5b50...9096f6 )
by Adam
01:59
created

BlameableSubscriber::setUserCallable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 1
			ORM\Events::loadClassMetadata,
66 1
			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 1
		$this->driver = $driver;
79 1
		$this->userCallable = $userCallable;
80 1
	}
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 1
		$classMetadata = $eventArgs->getClassMetadata();
93 1
		$this->driver->loadMetadataForObjectClass($eventArgs->getObjectManager(), $classMetadata);
94
95
		// Register pre persist event
96 1
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
97
		// Register pre update event
98 1
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
99
		// Register pre remove event
100 1
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
101 1
	}
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 1
		$em = $eventArgs->getEntityManager();
113 1
		$uow = $em->getUnitOfWork();
114
115
		// Check all scheduled updates
116 1
		foreach ($uow->getScheduledEntityUpdates() as $object) {
117
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
118 1
			$classMetadata = $em->getClassMetadata(get_class($object));
119
120 1
			if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
121 1
				$changeSet = $uow->getEntityChangeSet($object);
122 1
				$needChanges = FALSE;
123
124 1
				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 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...
137 1
					foreach ($config['update'] as $field) {
138 1
						$isInsertAndNull = $uow->isScheduledForInsert($object)
139 1
							&& array_key_exists($field, $changeSet)
140 1
							&& $changeSet[$field][1] === NULL;
141
142 1
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
143 1
							$needChanges = TRUE;
144 1
							$this->updateField($uow, $object, $classMetadata, $field);
145
						}
146
					}
147
				}
148
149 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...
150 1
					foreach ($config['delete'] as $field) {
151 1
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
152 1
							&& array_key_exists($field, $changeSet)
153 1
							&& $changeSet[$field][1] === NULL;
154
155 1
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
156 1
							$needChanges = TRUE;
157 1
							$this->updateField($uow, $object, $classMetadata, $field);
158
						}
159
					}
160
				}
161
162 1
				if (isset($config['change'])) {
163 1
					foreach ($config['change'] as $options) {
164 1
						if (isset($changeSet[$options['field']])) {
165 1
							continue; // Value was set manually
166
						}
167
168 1
						if (!is_array($options['trackedField'])) {
169 1
							$singleField = TRUE;
170 1
							$trackedFields = [$options['trackedField']];
171
172
						} else {
173
							$singleField = FALSE;
174
							$trackedFields = $options['trackedField'];
175
						}
176
177 1
						foreach ($trackedFields as $tracked) {
178 1
							$trackedChild = NULL;
179 1
							$parts = explode('.', $tracked);
180
181 1
							if (isset($parts[1])) {
182 1
								$tracked = $parts[0];
183 1
								$trackedChild = $parts[1];
184
							}
185
186 1
							if (isset($changeSet[$tracked])) {
187 1
								$changes = $changeSet[$tracked];
188
189 1
								if (isset($trackedChild)) {
190 1
									$changingObject = $changes[1];
191
192 1
									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 1
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
198 1
									$em->initializeObject($changingObject);
199 1
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
200
201
								} else {
202
									$value = $changes[1];
203
								}
204
205 1
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
206 1
									$needChanges = TRUE;
207 1
									$this->updateField($uow, $object, $classMetadata, $options['field']);
208
								}
209
							}
210
						}
211
					}
212
				}
213
214 1
				if ($needChanges) {
215 1
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
216
				}
217
			}
218
		}
219 1
	}
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 1
		$em = $eventArgs->getEntityManager();
232 1
		$uow = $em->getUnitOfWork();
233 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
234
235 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
236 1
			foreach(['update', 'create'] as $event) {
237 1
				if (isset($config[$event])) {
238 1
					$this->updateFields($config[$event], $uow, $entity, $classMetadata);
239
				}
240
			}
241
		}
242 1
	}
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 1
		$em = $eventArgs->getEntityManager();
255 1
		$uow = $em->getUnitOfWork();
256 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
257
258 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
259 1
			if (isset($config['update'])) {
260 1
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
261
			}
262
		}
263 1
	}
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 1
		$em = $eventArgs->getEntityManager();
276 1
		$uow = $em->getUnitOfWork();
277 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
278
279 1
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
280 1
			if (isset($config['delete'])) {
281 1
				$this->updateFields($config['delete'], $uow, $entity, $classMetadata);
282
			}
283
		}
284 1
	}
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 1
		$this->user = $user;
296 1
	}
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 1
		if ($this->user !== NULL) {
306 1
			return $this->user;
307
		}
308
309 1
		if ($this->userCallable === NULL) {
310
			return NULL;
311
		}
312
313 1
		$callable = $this->userCallable;
314
315 1
		return $callable();
316
	}
317
318
	/**
319
	 * @param callable $callable
320
	 *
321
	 * @return void
322
	 */
323
	public function setUserCallable(callable $callable) : void
324
	{
325 1
		$this->userCallable = $callable;
326 1
	}
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 1
		foreach ($fields as $field) {
339 1
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
340 1
				$this->updateField($uow, $object, $classMetadata, $field);
341
			}
342
		}
343 1
	}
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 1
		$property = $classMetadata->getReflectionProperty($field);
358
359 1
		$oldValue = $property->getValue($object);
360 1
		$newValue = $this->getUserValue($classMetadata, $field);
361
362 1
		$property->setValue($object, $newValue);
363
364 1
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
365 1
		$uow->scheduleExtraUpdate($object, [
366 1
			$field => [$oldValue, $newValue],
367
		]);
368 1
	}
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 1
		$user = $this->getUser();
381
382 1
		if ($classMetadata->hasAssociation($field)) {
383 1
			if ($user !== NULL && ! is_object($user)) {
384 1
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
385
			}
386
387 1
			return $user;
388
		}
389
390
		// Ok so its not an association, then it is a string
391 1
		if (is_object($user)) {
392 1
			if (method_exists($user, '__toString')) {
393
				return $user->__toString();
394
			}
395
396 1
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
397
		}
398
399 1
		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 1
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
413 1
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
414
		}
415 1
	}
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 1
		if (!isset($classMetadata->entityListeners[$eventName])) {
427 1
			return FALSE;
428
		}
429
430 1
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
431 1
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
432 1
				return TRUE;
433
			}
434
		}
435
436
		return FALSE;
437
	}
438
}
439