Completed
Push — master ( 0b44fb...425683 )
by Adam
35:30 queued 30:28
created

BlameableListener::prePersist()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 7
Bugs 0 Features 0
Metric Value
c 7
b 0
f 0
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.2
cc 4
eloc 8
nc 4
nop 2
crap 4
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);
0 ignored issues
show
Bug introduced by
The method updateField() does not exist on IPub\DoctrineBlameable\Events\BlameableListener. Did you maybe mean updateFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The method updateField() does not exist on IPub\DoctrineBlameable\Events\BlameableListener. Did you maybe mean updateFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
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
							&& $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);
0 ignored issues
show
Bug introduced by
The method updateField() does not exist on IPub\DoctrineBlameable\Events\BlameableListener. Did you maybe mean updateFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
159
						}
160 1
					}
161
				}
162
163
				if (isset($config['change'])) {
164
					foreach ($config['change'] as $options) {
165 1
						if (isset($changeSet[$options['field']])) {
166 1
							continue; // Value was set manually
167 1
						}
168
169 1
						if (!is_array($options['trackedField'])) {
170 1
							$singleField = TRUE;
171 1
							$trackedFields = [$options['trackedField']];
172 1
173
						} else {
174 1
							$singleField = FALSE;
175 1
							$trackedFields = $options['trackedField'];
176
						}
177 1
178 1
						foreach ($trackedFields as $tracked) {
179
							$trackedChild = NULL;
180 1
							$parts = explode('.', $tracked);
181
182
							if (isset($parts[1])) {
183
								$tracked = $parts[0];
184
								$trackedChild = $parts[1];
185 1
							}
186 1
187 1
							if (isset($changeSet[$tracked])) {
188
								$changes = $changeSet[$tracked];
189 1
190
								if (isset($trackedChild)) {
191
									$changingObject = $changes[1];
192
193 1
									if (!is_object($changingObject)) {
194 1
										throw new Exceptions\UnexpectedValueException("Field - [{$options['field']}] is expected to be object in class - {$classMetadata->getName()}");
195 1
									}
196 1
197 1
									/** @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 1
									$value = $changes[1];
204 1
								}
205 1
206 1
								if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === NULL) {
207 1
									$needChanges = TRUE;
208
									$this->updateField($uow, $object, $classMetadata, $options['field']);
0 ignored issues
show
Bug introduced by
The method updateField() does not exist on IPub\DoctrineBlameable\Events\BlameableListener. Did you maybe mean updateFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
209
								}
210
							}
211
						}
212
					}
213
				}
214
215 1
				if ($needChanges) {
216 1
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
217 1
				}
218
			}
219 1
		}
220 1
	}
221 1
222 1
	/**
223 1
	 * @param mixed $entity
224 1
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
225 1
	 */
226 1
	public function prePersist($entity, Doctrine\ORM\Event\LifecycleEventArgs $eventArgs)
227 1
	{
228 1
		$em = $eventArgs->getEntityManager();
229 1
		$uow = $em->getUnitOfWork();
230 1
		$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 1
		}
239 1
	}
240 1
241
	/**
242 1
	 * @param mixed $entity
243 1
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
244 1
	 */
245 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...
246
	{
247
		$em = $eventArgs->getEntityManager();
248 1
		$uow = $em->getUnitOfWork();
249 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
250 1
251 1
		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 1
	 * @param mixed $entity
260 1
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
261 1
	 */
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 1
	{
264 1
		$em = $eventArgs->getEntityManager();
265 1
		$uow = $em->getUnitOfWork();
266 1
		$classMetadata = $em->getClassMetadata(get_class($entity));
267 1
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
	}
274
275
	/**
276
	 * Set a custom representation of current user
277
	 *
278
	 * @param mixed $user
279
	 */
280
	public function setUser($user)
281 1
	{
282 1
		$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 1
	{
292 1
		if ($this->user !== NULL) {
293
			return $this->user;
294
		}
295 1
296
		if ($this->userCallable === NULL) {
297
			return;
298
		}
299 1
300
		$callable = $this->userCallable;
301 1
302
		return $callable();
303
	}
304
305
	/**
306
	 * @param callable $callable
307
	 */
308
	public function setUserCallable(callable $callable)
309 1
	{
310 1
		$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 1
			if ($classMetadata->getReflectionProperty($field)->getValue($object) === NULL) { // let manual values
323
				$property = $classMetadata->getReflectionProperty($field);
324 1
325 1
				$oldValue = $property->getValue($object);
326
				$newValue = $this->getUserValue($classMetadata, $field);
327 1
328
				$property->setValue($object, $newValue);
329 1
330 1
				$uow->propertyChanged($object, $field, $oldValue, $newValue);
331 1
				$uow->scheduleExtraUpdate($object, [
332 1
					$field => [$oldValue, $newValue],
333 1
				]);
334
			}
335
		}
336
	}
337
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 1
	 */
346
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, $field)
347 1
	{
348 1
		$user = $this->getUser();
349 1
350
		if ($classMetadata->hasAssociation($field)) {
351
			if ($user !== NULL && ! is_object($user)) {
352 1
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
353
			}
354
355
			return $user;
356 1
		}
357 1
358
		// Ok so its not an association, then it is a string
359
		if (is_object($user)) {
360
			if (method_exists($user, '__toString')) {
361 1
				return $user->__toString();
362
			}
363
364 1
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
365
		}
366
367
		return $user;
368
	}
369
370
	/**
371
	 * @param ORM\Mapping\ClassMetadata $classMetadata
372
	 * @param string $eventName
373
	 *
374
	 * @throws ORM\Mapping\MappingException
375 1
	 */
376 1
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, $eventName)
377 1
	{
378 1
		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 1
	 */
390 1
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, $eventName, $listenerClass)
391
	{
392
		if (!isset($classMetadata->entityListeners[$eventName])) {
393 1
			return FALSE;
394 1
		}
395 1
396
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
397
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
398
				return TRUE;
399
			}
400
		}
401
402 1
		return FALSE;
403
	}
404
}
405