Completed
Push — master ( b8314e...49f715 )
by Adam
03:36
created

BlameableListener::preUpdate()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 16
Ratio 100 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 16
loc 16
ccs 9
cts 9
cp 1
rs 8.8571
cc 5
eloc 9
nc 5
nop 2
crap 5
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
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
198 1
						$this->updateField($uow, $entity, $classMetadata, $field);
199 1
					}
200
				}
201 1
			}
202 1
203 1
			if (isset($config['create'])) {
204 1
				foreach ($config['create'] as $field) {
205 1
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
206 1
						$this->updateField($uow, $entity, $classMetadata, $field);
207 1
					}
208
				}
209
			}
210
		}
211
	}
212
213
	/**
214
	 * @param mixed $entity
215 1
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
216 1
	 */
217 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...
218
	{
219 1
		$em = $eventArgs->getEntityManager();
220 1
		$uow = $em->getUnitOfWork();
221 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
222 1
223 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
224 1
			if (isset($config['update'])) {
225 1
				foreach ($config['update'] as $field) {
226 1
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
227
						$this->updateField($uow, $entity, $classMetadata, $field);
228
					}
229
				}
230
			}
231
		}
232
	}
233
234 1
	/**
235 1
	 * @param mixed $entity
236 1
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
237
	 */
238 1 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...
239 1
	{
240 1
		$em = $eventArgs->getEntityManager();
241 1
		$uow = $em->getUnitOfWork();
242 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
243 1
244 1
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
245 1
			if (isset($config['delete'])) {
246
				foreach ($config['delete'] as $field) {
247
					if ($classMetadata->getReflectionProperty($field)->getValue($entity) === NULL) { // let manual values
248
						$this->updateField($uow, $entity, $classMetadata, $field);
249
					}
250
				}
251
			}
252
		}
253
	}
254 1
255 1
	/**
256
	 * Set a custom representation of current user
257
	 *
258
	 * @param mixed $user
259
	 */
260
	public function setUser($user)
261
	{
262
		$this->user = $user;
263
	}
264 1
265 1
	/**
266
	 * Get current user, either if $this->user is present or from userCallable
267
	 *
268 1
	 * @return mixed The user representation
269
	 */
270
	public function getUser()
271
	{
272 1
		if ($this->user !== NULL) {
273
			return $this->user;
274 1
		}
275
276
		if ($this->userCallable === NULL) {
277
			return;
278
		}
279
280
		$callable = $this->userCallable;
281
282 1
		return $callable();
283 1
	}
284
285
	/**
286
	 * @param callable $callable
287
	 */
288
	public function setUserCallable(callable $callable)
289
	{
290
		$this->userCallable = $callable;
291
	}
292
293
	/**
294
	 * Updates a field
295 1
	 *
296
	 * @param ORM\UnitOfWork $uow
297 1
	 * @param mixed $object
298 1
	 * @param ORM\Mapping\ClassMetadata $classMetadata
299
	 * @param string $field
300 1
	 */
301
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, $field)
302 1
	{
303 1
		$property = $classMetadata->getReflectionProperty($field);
304 1
305 1
		$oldValue = $property->getValue($object);
306 1
		$newValue = $this->getUserValue($classMetadata, $field);
307
308
		$property->setValue($object, $newValue);
309
310
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
311
		$uow->scheduleExtraUpdate($object, [
312
			$field => [$oldValue, $newValue],
313
		]);
314
	}
315
316
	/**
317
	 * Get the user value to set on a blameable field
318 1
	 *
319
	 * @param ORM\Mapping\ClassMetadata $classMetadata
320 1
	 * @param string $field
321 1
	 *
322 1
	 * @return mixed
323
	 */
324
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, $field)
325 1
	{
326
		$user = $this->getUser();
327
328
		if ($classMetadata->hasAssociation($field)) {
329 1
			if ($user !== NULL && ! is_object($user)) {
330 1
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
331
			}
332
333
			return $user;
334 1
		}
335
336
		// Ok so its not an association, then it is a string
337 1
		if (is_object($user)) {
338
			if (method_exists($user, '__toString')) {
339
				return $user->__toString();
340
			}
341
342
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
343
		}
344
345
		return $user;
346
	}
347
348 1
	/**
349 1
	 * @param ORM\Mapping\ClassMetadata $classMetadata
350 1
	 * @param string $eventName
351 1
	 *
352
	 * @throws ORM\Mapping\MappingException
353
	 */
354
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, $eventName)
355
	{
356
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
357
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
358
		}
359
	}
360
361
	/**
362 1
	 * @param ORM\Mapping\ClassMetadata $classMetadata
363 1
	 * @param string $eventName
364
	 * @param string $listenerClass
365
	 *
366 1
	 * @return bool
367 1
	 */
368 1
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, $eventName, $listenerClass)
369
	{
370
		if (!isset($classMetadata->entityListeners[$eventName])) {
371
			return FALSE;
372
		}
373
374
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
375 1
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
376
				return TRUE;
377
			}
378
		}
379
380
		return FALSE;
381
	}
382
}
383