Completed
Push — master ( d265e9...9b5ed2 )
by Adam
03:50
created

BlameableListener::prePersist()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 7.551
cc 7
eloc 12
nc 6
nop 2
crap 7
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
				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['change'])) {
151
					foreach ($config['change'] as $options) {
152
						if (isset($changeSet[$options['field']])) {
153
							continue; // Value was set manually
154
						}
155
156
						if (!is_array($options['trackedField'])) {
157
							$singleField = TRUE;
158
							$trackedFields = [$options['trackedField']];
159
160
						} else {
161
							$singleField = FALSE;
162
							$trackedFields = $options['trackedField'];
163
						}
164
165
						foreach ($trackedFields as $tracked) {
166
							$trackedChild = NULL;
167
							$parts = explode('.', $tracked);
168
169
							if (isset($parts[1])) {
170
								$tracked = $parts[0];
171
								$trackedChild = $parts[1];
172
							}
173
174
							if (isset($changeSet[$tracked])) {
175
								$changes = $changeSet[$tracked];
176
177 1
								if (isset($trackedChild)) {
178
									$changingObject = $changes[1];
179
180 1
									if (!is_object($changingObject)) {
181 1
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
182 1
									}
183
184
									/** @var ORM\Mapping\ClassMetadata $objectMeta */
185
									$objectMeta = $em->getClassMetadata(get_class($changingObject));
186
									$em->initializeObject($changingObject);
187
									$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
188
189
								} else {
190 1
									$value = $changes[1];
191 1
								}
192 1
193
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
194 1
									$needChanges = TRUE;
195 1
									$this->updateField($uow, $object, $classMetadata, $options['field']);
196 1
								}
197 1
							}
198 1
						}
199 1
					}
200
				}
201 1
202 1
				if ($needChanges) {
203 1
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
204 1
				}
205 1
			}
206 1
		}
207 1
	}
208
209
	/**
210
	 * @param mixed $entity
211
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
212
	 */
213
	public function prePersist($entity, Doctrine\ORM\Event\LifecycleEventArgs $eventArgs)
214
	{
215 1
		$em = $eventArgs->getEntityManager();
216 1
		$uow = $em->getUnitOfWork();
217 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
218
219 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
220 1
			foreach(['update', 'create'] as $event) {
221 1
				if (isset($config[$event])) {
222 1
					foreach ($config[$event] as $field) {
223 1
						$currentValue = $classMetadata->getReflectionProperty($field)->getValue($entity);
224 1
						$newValue = $this->getUserValue($classMetadata, $field);
225 1
226 1
						if ($currentValue === NULL || $currentValue !== $newValue) { // let manual values
227
							$this->updateField($uow, $entity, $classMetadata, $field);
228
						}
229
					}
230
				}
231
			}
232
		}
233
	}
234 1
235 1
	/**
236 1
	 * @param mixed $entity
237
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
238 1
	 */
239 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...
240 1
	{
241 1
		$em = $eventArgs->getEntityManager();
242 1
		$uow = $em->getUnitOfWork();
243 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
244 1
245 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
246
			if (isset($config['update'])) {
247
				foreach ($config['update'] as $field) {
248
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
249
						$this->updateField($uow, $entity, $classMetadata, $field);
250
					}
251
				}
252
			}
253
		}
254 1
	}
255 1
256
	/**
257
	 * @param mixed $entity
258
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
259
	 */
260 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...
261
	{
262
		$em = $eventArgs->getEntityManager();
263
		$uow = $em->getUnitOfWork();
264 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
265 1
266
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
267
			if (isset($config['delete'])) {
268 1
				foreach ($config['delete'] as $field) {
269
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
270
						$this->updateField($uow, $entity, $classMetadata, $field);
271
					}
272 1
				}
273
			}
274 1
		}
275
	}
276
277
	/**
278
	 * Set a custom representation of current user
279
	 *
280
	 * @param mixed $user
281
	 */
282 1
	public function setUser($user)
283 1
	{
284
		$this->user = $user;
285
	}
286
287
	/**
288
	 * Get current user, either if $this->user is present or from userCallable
289
	 *
290
	 * @return mixed The user representation
291
	 */
292
	public function getUser()
293
	{
294
		if ($this->user !== NULL) {
295 1
			return $this->user;
296
		}
297 1
298 1
		if ($this->userCallable === NULL) {
299
			return;
300 1
		}
301
302 1
		$callable = $this->userCallable;
303 1
304 1
		return $callable();
305 1
	}
306 1
307
	/**
308
	 * @param callable $callable
309
	 */
310
	public function setUserCallable(callable $callable)
311
	{
312
		$this->userCallable = $callable;
313
	}
314
315
	/**
316
	 * Updates a field
317
	 *
318 1
	 * @param ORM\UnitOfWork $uow
319
	 * @param mixed $object
320 1
	 * @param ORM\Mapping\ClassMetadata $classMetadata
321 1
	 * @param string $field
322 1
	 */
323
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, $field)
324
	{
325 1
		$property = $classMetadata->getReflectionProperty($field);
326
327
		$oldValue = $property->getValue($object);
328
		$newValue = $this->getUserValue($classMetadata, $field);
329 1
330 1
		$property->setValue($object, $newValue);
331
332
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
333
		$uow->scheduleExtraUpdate($object, [
334 1
			$field => [$oldValue, $newValue],
335
		]);
336
	}
337 1
338
	/**
339
	 * Get the user value to set on a blameable field
340
	 *
341
	 * @param ORM\Mapping\ClassMetadata $classMetadata
342
	 * @param string $field
343
	 *
344
	 * @return mixed
345
	 */
346
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, $field)
347
	{
348 1
		$user = $this->getUser();
349 1
350 1
		if ($classMetadata->hasAssociation($field)) {
351 1
			if ($user !== NULL && ! is_object($user)) {
352
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
353
			}
354
355
			return $user;
356
		}
357
358
		// Ok so its not an association, then it is a string
359
		if (is_object($user)) {
360
			if (method_exists($user, '__toString')) {
361
				return $user->__toString();
362 1
			}
363 1
364
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
365
		}
366 1
367 1
		return $user;
368 1
	}
369
370
	/**
371
	 * @param ORM\Mapping\ClassMetadata $classMetadata
372
	 * @param string $eventName
373
	 *
374
	 * @throws ORM\Mapping\MappingException
375 1
	 */
376
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, $eventName)
377
	{
378
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
379
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
380
		}
381
	}
382
383
	/**
384
	 * @param ORM\Mapping\ClassMetadata $classMetadata
385
	 * @param string $eventName
386
	 * @param string $listenerClass
387
	 *
388
	 * @return bool
389
	 */
390
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, $eventName, $listenerClass)
391
	{
392
		if (!isset($classMetadata->entityListeners[$eventName])) {
393
			return FALSE;
394
		}
395
396
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
397
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
398
				return TRUE;
399
			}
400
		}
401
402
		return FALSE;
403
	}
404
}
405