Test Failed
Pull Request — master (#3)
by Adam
01:54
created

BlameableSubscriber::__construct()   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 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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 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
		?callable $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
				if (isset($config['update'])) {
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
				if (isset($config['delete'])) {
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
	public function preUpdate($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
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
	public function preRemove($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
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