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

BlameableSubscriber   C

Complexity

Total Complexity 68

Size/Duplication

Total Lines 393
Duplicated Lines 12.21 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 91.18%

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 10
dl 48
loc 393
ccs 124
cts 136
cp 0.9118
rs 5.6756
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setUserCallable() 0 4 1
A getSubscribedEvents() 0 7 1
A __construct() 0 7 1
A loadClassMetadata() 0 13 1
D onFlush() 24 110 33
A prePersist() 0 14 4
A preUpdate() 12 12 3
A preRemove() 12 12 3
A setUser() 0 4 1
A getUser() 0 14 3
A updateFields() 0 8 3
A updateField() 0 14 1
B getUserValue() 0 23 6
A registerEvent() 0 6 2
B hasRegisteredListener() 0 14 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like BlameableSubscriber often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BlameableSubscriber, and based on these observations, apply Extract Interface, too.

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