Completed
Push — master ( f3cdba...60afca )
by Adam
04:43
created

BlameableSubscriber::onFlush()   D

Complexity

Conditions 33
Paths 66

Size

Total Lines 110
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 110
rs 4.3466
cc 33
eloc 62
nc 66
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
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 subscriber
34
 *
35
 * @package        iPublikuj:DoctrineBlameable!
36
 * @subpackage     Events
37
 *
38
 * @author         Adam Kadlec <[email protected]>
39
 */
40
final class BlameableSubscriber 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
			'Doctrine\\ORM\\Event::loadClassMetadata',
71
			'Doctrine\\ORM\\Event::onFlush',
72
		];
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
		$this->driver = $driver;
84
		$this->userCallable = $userCallable;
85
	}
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
		$classMetadata = $eventArgs->getClassMetadata();
96
		$this->driver->loadMetadataForObjectClass($classMetadata);
97
98
		// Register pre persist event
99
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
100
		// Register pre update event
101
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
102
		// Register pre remove event
103
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
104
	}
105
106
	/**
107
	 * @param ORM\Event\OnFlushEventArgs $eventArgs
108
	 *
109
	 * @throws Exceptions\UnexpectedValueException
110
	 */
111
	public function onFlush(ORM\Event\OnFlushEventArgs $eventArgs)
112
	{
113
		$em = $eventArgs->getEntityManager();
114
		$uow = $em->getUnitOfWork();
115
116
		// Check all scheduled updates
117
		foreach ($uow->getScheduledEntityUpdates() as $object) {
118
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
119
			$classMetadata = $em->getClassMetadata(get_class($object));
120
121
			if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
122
				$changeSet = $uow->getEntityChangeSet($object);
123
				$needChanges = FALSE;
124
125
				if ($uow->isScheduledForInsert($object) && isset($config['create'])) {
126
					foreach ($config['create'] as $field) {
127
						// Field can not exist in change set, when persisting embedded document without parent for example
128
						$new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : FALSE;
129
130
						if ($new === NULL) { // let manual values
131
							$needChanges = TRUE;
132
							$this->updateField($uow, $object, $classMetadata, $field);
133
						}
134
					}
135
				}
136
137
				if (isset($config['update'])) {
138
					foreach ($config['update'] as $field) {
139
						$isInsertAndNull = $uow->isScheduledForInsert($object)
140
							&& array_key_exists($field, $changeSet)
141
							&& $changeSet[$field][1] === NULL;
142
143
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
144
							$needChanges = TRUE;
145
							$this->updateField($uow, $object, $classMetadata, $field);
146
						}
147
					}
148
				}
149
150
				if (isset($config['delete'])) {
151
					foreach ($config['delete'] as $field) {
152
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
153
							&& array_key_exists($field, $changeSet)
154
							&& $changeSet[$field][1] === NULL;
155
156
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
157
							$needChanges = TRUE;
158
							$this->updateField($uow, $object, $classMetadata, $field);
159
						}
160
					}
161
				}
162
163
				if (isset($config['change'])) {
164
					foreach ($config['change'] as $options) {
165
						if (isset($changeSet[$options['field']])) {
166
							continue; // Value was set manually
167
						}
168
169
						if (!is_array($options['trackedField'])) {
170
							$singleField = TRUE;
171
							$trackedFields = [$options['trackedField']];
172
173
						} else {
174
							$singleField = FALSE;
175
							$trackedFields = $options['trackedField'];
176
						}
177
178
						foreach ($trackedFields as $tracked) {
179
							$trackedChild = NULL;
180
							$parts = explode('.', $tracked);
181
182
							if (isset($parts[1])) {
183
								$tracked = $parts[0];
184
								$trackedChild = $parts[1];
185
							}
186
187
							if (isset($changeSet[$tracked])) {
188
								$changes = $changeSet[$tracked];
189
190
								if (isset($trackedChild)) {
191
									$changingObject = $changes[1];
192
193
									if (!is_object($changingObject)) {
194
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
195
									}
196
197
									/** @var ORM\Mapping\ClassMetadata $objectMeta */
198
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
199
									$em->initializeObject($changingObject);
200
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
201
202
								} else {
203
									$value = $changes[1];
204
								}
205
206
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
207
									$needChanges = TRUE;
208
									$this->updateField($uow, $object, $classMetadata, $options['field']);
209
								}
210
							}
211
						}
212
					}
213
				}
214
215
				if ($needChanges) {
216
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
217
				}
218
			}
219
		}
220
	}
221
222
	/**
223
	 * @param mixed $entity
224
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
225
	 */
226
	public function prePersist($entity, Doctrine\ORM\Event\LifecycleEventArgs $eventArgs)
227
	{
228
		$em = $eventArgs->getEntityManager();
229
		$uow = $em->getUnitOfWork();
230
		$classMetadata = $em->getClassMetadata(get_class($entity));
231
232
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
233
			foreach(['update', 'create'] as $event) {
234
				if (isset($config[$event])) {
235
					$this->updateFields($config[$event], $uow, $entity, $classMetadata);
236
				}
237
			}
238
		}
239
	}
240
241
	/**
242
	 * @param mixed $entity
243
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
244
	 */
245 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...
246
	{
247
		$em = $eventArgs->getEntityManager();
248
		$uow = $em->getUnitOfWork();
249
		$classMetadata = $em->getClassMetadata(get_class($entity));
250
251
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
252
			if (isset($config['update'])) {
253
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
254
			}
255
		}
256
	}
257
258
	/**
259
	 * @param mixed $entity
260
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
261
	 */
262 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...
263
	{
264
		$em = $eventArgs->getEntityManager();
265
		$uow = $em->getUnitOfWork();
266
		$classMetadata = $em->getClassMetadata(get_class($entity));
267
268
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
269
			if (isset($config['delete'])) {
270
				$this->updateFields($config['delete'], $uow, $entity, $classMetadata);
271
			}
272
		}
273
	}
274
275
	/**
276
	 * Set a custom representation of current user
277
	 *
278
	 * @param mixed $user
279
	 */
280
	public function setUser($user)
281
	{
282
		$this->user = $user;
283
	}
284
285
	/**
286
	 * Get current user, either if $this->user is present or from userCallable
287
	 *
288
	 * @return mixed The user representation
289
	 */
290
	public function getUser()
291
	{
292
		if ($this->user !== NULL) {
293
			return $this->user;
294
		}
295
296
		if ($this->userCallable === NULL) {
297
			return NULL;
298
		}
299
300
		$callable = $this->userCallable;
301
302
		return $callable();
303
	}
304
305
	/**
306
	 * @param callable $callable
307
	 */
308
	public function setUserCallable(callable $callable)
309
	{
310
		$this->userCallable = $callable;
311
	}
312
313
	/**
314
	 * @param array $fields
315
	 * @param ORM\UnitOfWork $uow
316
	 * @param mixed $object
317
	 * @param ORM\Mapping\ClassMetadata $classMetadata
318
	 */
319
	private function updateFields(array $fields, ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata)
320
	{
321
		foreach ($fields as $field) {
322
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
323
				$this->updateField($uow, $object, $classMetadata, $field);
324
			}
325
		}
326
	}
327
328
	/**
329
	 * Updates a field
330
	 *
331
	 * @param ORM\UnitOfWork $uow
332
	 * @param mixed $object
333
	 * @param ORM\Mapping\ClassMetadata $classMetadata
334
	 * @param string $field
335
	 */
336
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, $field)
337
	{
338
		$property = $classMetadata->getReflectionProperty($field);
339
340
		$oldValue = $property->getValue($object);
341
		$newValue = $this->getUserValue($classMetadata, $field);
342
343
		$property->setValue($object, $newValue);
344
345
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
346
		$uow->scheduleExtraUpdate($object, [
347
			$field => [$oldValue, $newValue],
348
		]);
349
	}
350
351
	/**
352
	 * Get the user value to set on a blameable field
353
	 *
354
	 * @param ORM\Mapping\ClassMetadata $classMetadata
355
	 * @param string $field
356
	 *
357
	 * @return mixed
358
	 */
359
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, $field)
360
	{
361
		$user = $this->getUser();
362
363
		if ($classMetadata->hasAssociation($field)) {
364
			if ($user !== NULL && ! is_object($user)) {
365
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
366
			}
367
368
			return $user;
369
		}
370
371
		// Ok so its not an association, then it is a string
372
		if (is_object($user)) {
373
			if (method_exists($user, '__toString')) {
374
				return $user->__toString();
375
			}
376
377
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
378
		}
379
380
		return $user;
381
	}
382
383
	/**
384
	 * @param ORM\Mapping\ClassMetadata $classMetadata
385
	 * @param string $eventName
386
	 *
387
	 * @throws ORM\Mapping\MappingException
388
	 */
389
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, $eventName)
390
	{
391
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
392
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
393
		}
394
	}
395
396
	/**
397
	 * @param ORM\Mapping\ClassMetadata $classMetadata
398
	 * @param string $eventName
399
	 * @param string $listenerClass
400
	 *
401
	 * @return bool
402
	 */
403
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, $eventName, $listenerClass)
404
	{
405
		if (!isset($classMetadata->entityListeners[$eventName])) {
406
			return FALSE;
407
		}
408
409
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
410
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
411
				return TRUE;
412
			}
413
		}
414
415
		return FALSE;
416
	}
417
}
418