Completed
Push — master ( 49f715...d265e9 )
by Adam
03:40
created

BlameableListener   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 355
Duplicated Lines 10.7 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 70.92%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 60
c 5
b 0
f 0
lcom 1
cbo 10
dl 38
loc 355
ccs 100
cts 141
cp 0.7092
rs 6.0976

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getSubscribedEvents() 0 7 1
A __construct() 0 7 1
A loadClassMetadata() 0 13 1
C onFlush() 0 72 16
D prePersist() 0 30 10
B preUpdate() 19 19 6
B preRemove() 19 19 6
A setUser() 0 4 1
A getUser() 0 14 3
A setUserCallable() 0 4 1
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 BlameableListener 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 BlameableListener, and based on these observations, apply Extract Interface, too.

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