Completed
Push — master ( a1caa1...4413c9 )
by Adam
07:11
created

BlameableSubscriber::setUserCallable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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