Completed
Push — master ( 425683...464b49 )
by Adam
36:43 queued 33:11
created

BlameableListener::updateField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 14
ccs 9
cts 9
cp 1
rs 9.4286
cc 1
eloc 8
nc 1
nop 4
crap 1
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 ($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 1
				if (isset($config['update'])) {
138 1
					foreach ($config['update'] as $field) {
139 1
						$isInsertAndNull = $uow->isScheduledForInsert($object)
140 1
							&& array_key_exists($field, $changeSet)
141 1
							&& $changeSet[$field][1] === NULL;
142
143 1
						if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
144 1
							$needChanges = TRUE;
145 1
							$this->updateField($uow, $object, $classMetadata, $field);
146 1
						}
147 1
					}
148 1
				}
149
150 1
				if (isset($config['delete'])) {
151 1
					foreach ($config['delete'] as $field) {
152 1
						$isDeleteAndNull = $uow->isScheduledForDelete($object)
153 1
							&& array_key_exists($field, $changeSet)
154 1
							&& $changeSet[$field][1] === NULL;
155
156 1
						if (!isset($changeSet[$field]) || $isDeleteAndNull) { // let manual values
157 1
							$needChanges = TRUE;
158 1
							$this->updateField($uow, $object, $classMetadata, $field);
159 1
						}
160 1
					}
161 1
				}
162
163 1
				if (isset($config['change'])) {
164 1
					foreach ($config['change'] as $options) {
165 1
						if (isset($changeSet[$options['field']])) {
166 1
							continue; // Value was set manually
167
						}
168
169 1
						if (!is_array($options['trackedField'])) {
170 1
							$singleField = TRUE;
171 1
							$trackedFields = [$options['trackedField']];
172
173 1
						} else {
174
							$singleField = FALSE;
175
							$trackedFields = $options['trackedField'];
176
						}
177
178 1
						foreach ($trackedFields as $tracked) {
179 1
							$trackedChild = NULL;
180 1
							$parts = explode('.', $tracked);
181
182 1
							if (isset($parts[1])) {
183 1
								$tracked = $parts[0];
184 1
								$trackedChild = $parts[1];
185 1
							}
186
187 1
							if (isset($changeSet[$tracked])) {
188 1
								$changes = $changeSet[$tracked];
189
190 1
								if (isset($trackedChild)) {
191 1
									$changingObject = $changes[1];
192
193 1
									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 1
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
199 1
									$em->initializeObject($changingObject);
200 1
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
201
202 1
								} else {
203
									$value = $changes[1];
204
								}
205
206 1
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
207 1
									$needChanges = TRUE;
208 1
									$this->updateField($uow, $object, $classMetadata, $options['field']);
209 1
								}
210 1
							}
211 1
						}
212 1
					}
213 1
				}
214
215 1
				if ($needChanges) {
216 1
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
217 1
				}
218 1
			}
219 1
		}
220 1
	}
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 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($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 1
				}
237 1
			}
238 1
		}
239 1
	}
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 1
		$em = $eventArgs->getEntityManager();
248 1
		$uow = $em->getUnitOfWork();
249 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
250
251 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
252 1
			if (isset($config['update'])) {
253 1
				$this->updateFields($config['update'], $uow, $entity, $classMetadata);
254 1
			}
255 1
		}
256 1
	}
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 1
		$em = $eventArgs->getEntityManager();
265 1
		$uow = $em->getUnitOfWork();
266 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
267
268 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
269 1
			if (isset($config['delete'])) {
270 1
				$this->updateFields($config['delete'], $uow, $entity, $classMetadata);
271 1
			}
272 1
		}
273 1
	}
274
275
	/**
276
	 * Set a custom representation of current user
277
	 *
278
	 * @param mixed $user
279
	 */
280
	public function setUser($user)
281
	{
282 1
		$this->user = $user;
283 1
	}
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 1
		if ($this->user !== NULL) {
293 1
			return $this->user;
294
		}
295
296 1
		if ($this->userCallable === NULL) {
297
			return;
298
		}
299
300 1
		$callable = $this->userCallable;
301
302 1
		return $callable();
303
	}
304
305
	/**
306
	 * @param callable $callable
307
	 */
308
	public function setUserCallable(callable $callable)
309
	{
310 1
		$this->userCallable = $callable;
311 1
	}
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 1
		foreach ($fields as $field) {
322 1
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
323 1
				$this->updateField($uow, $object, $classMetadata, $field);
324 1
			}
325 1
		}
326 1
	}
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 1
		$property = $classMetadata->getReflectionProperty($field);
339
340 1
		$oldValue = $property->getValue($object);
341 1
		$newValue = $this->getUserValue($classMetadata, $field);
342
343 1
		$property->setValue($object, $newValue);
344
345 1
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
346 1
		$uow->scheduleExtraUpdate($object, [
347 1
			$field => [$oldValue, $newValue],
348 1
		]);
349 1
	}
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 1
		$user = $this->getUser();
362
363 1
		if ($classMetadata->hasAssociation($field)) {
364 1
			if ($user !== NULL && ! is_object($user)) {
365 1
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
366
			}
367
368 1
			return $user;
369
		}
370
371
		// Ok so its not an association, then it is a string
372 1
		if (is_object($user)) {
373 1
			if (method_exists($user, '__toString')) {
374
				return $user->__toString();
375
			}
376
377 1
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
378
		}
379
380 1
		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 1
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
392 1
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
393 1
		}
394 1
	}
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 1
		if (!isset($classMetadata->entityListeners[$eventName])) {
406 1
			return FALSE;
407
		}
408
409 1
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
410 1
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
411 1
				return TRUE;
412
			}
413
		}
414
415
		return FALSE;
416
	}
417
}
418