BlameableSubscriber::preUpdate()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 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
final class BlameableSubscriber implements Common\EventSubscriber
36
{
37
	/**
38
	 * Implement nette smart magic
39
	 */
40
	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
		?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 Common\Annotations\AnnotationException
88
	 * @throws ORM\Mapping\MappingException
89
	 */
90
	public function loadClassMetadata(ORM\Event\LoadClassMetadataEventArgs $eventArgs) : void
91
	{
92
		/** @var ORM\Mapping\ClassMetadata $classMetadata */
93
		$classMetadata = $eventArgs->getClassMetadata();
94
		$this->driver->loadMetadataForObjectClass($eventArgs->getObjectManager(), $classMetadata);
0 ignored issues
show
Compatibility introduced by
$eventArgs->getObjectManager() of type object<Doctrine\Persistence\ObjectManager> is not a sub-type of object<Doctrine\Common\Persistence\ObjectManager>. It seems like you assume a child interface of the interface Doctrine\Persistence\ObjectManager to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
95
96
		// Register pre persist event
97
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
98
		// Register pre update event
99
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
100
		// Register pre remove event
101
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
102
	}
103
104
	/**
105
	 * @param ORM\Event\OnFlushEventArgs $eventArgs
106
	 *
107
	 * @return void
108
	 *
109
	 * @throws Common\Annotations\AnnotationException
110
	 * @throws ORM\Mapping\MappingException
111
	 */
112
	public function onFlush(ORM\Event\OnFlushEventArgs $eventArgs) : void
113
	{
114
		$em = $eventArgs->getEntityManager();
115
		$uow = $em->getUnitOfWork();
116
117
		// Check all scheduled updates
118
		foreach ($uow->getScheduledEntityUpdates() as $object) {
119
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
120
			$classMetadata = $em->getClassMetadata(get_class($object));
121
122
			if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
0 ignored issues
show
Documentation introduced by
$em is of type object<Doctrine\ORM\EntityManager>, but the function expects a object<Doctrine\Common\Persistence\ObjectManager>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
123
				$changeSet = $uow->getEntityChangeSet($object);
124
				$needChanges = FALSE;
125
126
				if ($uow->isScheduledForInsert($object) && isset($config['create'])) {
127
					foreach ($config['create'] as $field) {
128
						// Field can not exist in change set, when persisting embedded document without parent for example
129
						$new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : FALSE;
130
131
						if ($new === NULL) { // let manual values
132
							$needChanges = TRUE;
133
							$this->updateField($uow, $object, $classMetadata, $field);
134
						}
135
					}
136
				}
137
138
				if (isset($config['update'])) {
139
					foreach ($config['update'] as $field) {
140
						$isInsertAndNull = $uow->isScheduledForInsert($object)
141
							&& array_key_exists($field, $changeSet)
142
							&& $changeSet[$field][1] === NULL;
143
144
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
145
							$needChanges = TRUE;
146
							$this->updateField($uow, $object, $classMetadata, $field);
147
						}
148
					}
149
				}
150
151
				if (isset($config['delete'])) {
152
					foreach ($config['delete'] as $field) {
153
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
154
							&& array_key_exists($field, $changeSet)
155
							&& $changeSet[$field][1] === NULL;
156
157
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
158
							$needChanges = TRUE;
159
							$this->updateField($uow, $object, $classMetadata, $field);
160
						}
161
					}
162
				}
163
164
				if (isset($config['change'])) {
165
					foreach ($config['change'] as $options) {
166
						if (isset($changeSet[$options['field']])) {
167
							continue; // Value was set manually
168
						}
169
170
						if (!is_array($options['trackedField'])) {
171
							$singleField = TRUE;
172
							$trackedFields = [$options['trackedField']];
173
174
						} else {
175
							$singleField = FALSE;
176
							$trackedFields = $options['trackedField'];
177
						}
178
179
						foreach ($trackedFields as $tracked) {
180
							$trackedChild = NULL;
181
							$parts = explode('.', $tracked);
182
183
							if (isset($parts[1])) {
184
								$tracked = $parts[0];
185
								$trackedChild = $parts[1];
186
							}
187
188
							if (isset($changeSet[$tracked])) {
189
								$changes = $changeSet[$tracked];
190
191
								if (isset($trackedChild)) {
192
									$changingObject = $changes[1];
193
194
									if (!is_object($changingObject)) {
195
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
196
									}
197
198
									/** @var ORM\Mapping\ClassMetadata $objectMeta */
199
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
200
									$em->initializeObject($changingObject);
201
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
202
203
								} else {
204
									$value = $changes[1];
205
								}
206
207
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
208
									$needChanges = TRUE;
209
									$this->updateField($uow, $object, $classMetadata, $options['field']);
210
								}
211
							}
212
						}
213
					}
214
				}
215
216
				if ($needChanges) {
217
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
218
				}
219
			}
220
		}
221
	}
222
223
	/**
224
	 * @param mixed $entity
225
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
226
	 *
227
	 * @return void
228
	 *
229
	 * @throws Common\Annotations\AnnotationException
230
	 * @throws ORM\Mapping\MappingException
231
	 */
232
	public function prePersist($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
233
	{
234
		$em = $eventArgs->getEntityManager();
235
		$uow = $em->getUnitOfWork();
236
		$classMetadata = $em->getClassMetadata(get_class($entity));
237
238
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
0 ignored issues
show
Documentation introduced by
$em is of type object<Doctrine\ORM\EntityManager>, but the function expects a object<Doctrine\Common\Persistence\ObjectManager>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
239
			foreach (['update', 'create'] as $event) {
240
				if (isset($config[$event])) {
241
					$this->updateFields($config[$event], $uow, $entity, $classMetadata);
242
				}
243
			}
244
		}
245
	}
246
247
	/**
248
	 * @param mixed $entity
249
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
250
	 *
251
	 * @return void
252
	 *
253
	 * @throws Common\Annotations\AnnotationException
254
	 * @throws ORM\Mapping\MappingException
255
	 */
256
	public function preUpdate($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
257
	{
258
		$em = $eventArgs->getEntityManager();
259
		$uow = $em->getUnitOfWork();
260
		$classMetadata = $em->getClassMetadata(get_class($entity));
261
262
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
0 ignored issues
show
Documentation introduced by
$em is of type object<Doctrine\ORM\EntityManager>, but the function expects a object<Doctrine\Common\Persistence\ObjectManager>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
263
			if (isset($config['update'])) {
264
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
265
			}
266
		}
267
	}
268
269
	/**
270
	 * @param mixed $entity
271
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
272
	 *
273
	 * @return void
274
	 *
275
	 * @throws Common\Annotations\AnnotationException
276
	 * @throws ORM\Mapping\MappingException
277
	 */
278
	public function preRemove($entity, ORM\Event\LifecycleEventArgs $eventArgs) : void
279
	{
280
		$em = $eventArgs->getEntityManager();
281
		$uow = $em->getUnitOfWork();
282
		$classMetadata = $em->getClassMetadata(get_class($entity));
283
284
		if ($config = $this->driver->getObjectConfigurations($em, $classMetadata->getName())) {
0 ignored issues
show
Documentation introduced by
$em is of type object<Doctrine\ORM\EntityManager>, but the function expects a object<Doctrine\Common\Persistence\ObjectManager>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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