Completed
Push — master ( 75ad9d...ad7692 )
by Adam
10:40
created

BlameableListener::getCacheId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 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
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
			'Doctrine\\ORM\\Event::loadClassMetadata',
71
			'Doctrine\\ORM\\Event::onFlush',
72
		];
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
		$this->driver = $driver;
84
		$this->userCallable = $userCallable;
85
	}
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
		$classMetadata = $eventArgs->getClassMetadata();
96
		$this->driver->loadMetadataForObjectClass($classMetadata);
97
98
		// Register pre persist event
99
		$this->registerEvent($classMetadata, ORM\Events::prePersist);
100
		// Register pre update event
101
		$this->registerEvent($classMetadata, ORM\Events::preUpdate);
102
		// Register pre remove event
103
		$this->registerEvent($classMetadata, ORM\Events::preRemove);
104
	}
105
106
	/**
107
	 * @param ORM\Event\OnFlushEventArgs $eventArgs
108
	 *
109
	 * @throws Exceptions\UnexpectedValueException
110
	 */
111
	public function onFlush(ORM\Event\OnFlushEventArgs $eventArgs)
112
	{
113
		$em = $eventArgs->getEntityManager();
114
		$uow = $em->getUnitOfWork();
115
116
		// Check all scheduled updates
117
		foreach ($uow->getScheduledEntityUpdates() as $object) {
118
			/** @var ORM\Mapping\ClassMetadata $classMetadata */
119
			$classMetadata = $em->getClassMetadata(get_class($object));
120
121
			if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
122
				$changeSet = $uow->getEntityChangeSet($object);
123
				$needChanges = FALSE;
124
125
				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
				if ($needChanges) {
178
					$uow->recomputeSingleEntityChangeSet($classMetadata, $object);
179
				}
180
			}
181
		}
182
	}
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
		$em = $eventArgs->getEntityManager();
191
		$uow = $em->getUnitOfWork();
192
		$classMetadata = $em->getClassMetadata(get_class($entity));
193
194
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
195
			if (isset($config['update'])) {
196
				foreach ($config['update'] as $field) {
197
					$this->updateField($uow, $entity, $classMetadata, $field);
198
				}
199
			}
200
201
			if (isset($config['create'])) {
202
				foreach ($config['create'] as $field) {
203
					$this->updateField($uow, $entity, $classMetadata, $field);
204
				}
205
			}
206
		}
207
	}
208
209
	/**
210
	 * @param mixed $entity
211
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
212
	 */
213 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...
214
	{
215
		$em = $eventArgs->getEntityManager();
216
		$uow = $em->getUnitOfWork();
217
		$classMetadata = $em->getClassMetadata(get_class($entity));
218
219
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
220
			if (isset($config['update'])) {
221
				foreach ($config['update'] as $field) {
222
					$this->updateField($uow, $entity, $classMetadata, $field);
223
				}
224
			}
225
		}
226
	}
227
228
	/**
229
	 * @param mixed $entity
230
	 * @param ORM\Event\LifecycleEventArgs $eventArgs
231
	 */
232 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...
233
	{
234
		$em = $eventArgs->getEntityManager();
235
		$uow = $em->getUnitOfWork();
236
		$classMetadata = $em->getClassMetadata(get_class($entity));
237
238
		if ($config = $this->driver->getObjectConfigurations($classMetadata->getName())) {
239
			if (isset($config['delete'])) {
240
				foreach ($config['delete'] as $field) {
241
					$this->updateField($uow, $entity, $classMetadata, $field);
242
				}
243
			}
244
		}
245
	}
246
247
	/**
248
	 * Set a custom representation of current user
249
	 *
250
	 * @param mixed $user
251
	 */
252
	public function setUser($user)
253
	{
254
		$this->user = $user;
255
	}
256
257
	/**
258
	 * Get current user, either if $this->user is present or from userCallable
259
	 *
260
	 * @return mixed The user representation
261
	 */
262
	public function getUser()
263
	{
264
		if ($this->user !== NULL) {
265
			return $this->user;
266
		}
267
268
		if ($this->userCallable === NULL) {
269
			return;
270
		}
271
272
		$callable = $this->userCallable;
273
274
		return $callable();
275
	}
276
277
	/**
278
	 * @param callable $callable
279
	 */
280
	public function setUserCallable(callable $callable)
281
	{
282
		$this->userCallable = $callable;
283
	}
284
285
	/**
286
	 * Updates a field
287
	 *
288
	 * @param ORM\UnitOfWork $uow
289
	 * @param mixed $object
290
	 * @param ORM\Mapping\ClassMetadata $classMetadata
291
	 * @param string $field
292
	 */
293
	private function updateField(ORM\UnitOfWork $uow, $object, ORM\Mapping\ClassMetadata $classMetadata, $field)
294
	{
295
		$property = $classMetadata->getReflectionProperty($field);
296
297
		$oldValue = $property->getValue($object);
298
		$newValue = $this->getUserValue($classMetadata, $field);
299
300
		$property->setValue($object, $newValue);
301
302
		$uow->propertyChanged($object, $field, $oldValue, $newValue);
303
		$uow->scheduleExtraUpdate($object, [
304
			$field => [$oldValue, $newValue],
305
		]);
306
	}
307
308
	/**
309
	 * Get the user value to set on a blameable field
310
	 *
311
	 * @param ORM\Mapping\ClassMetadata $classMetadata
312
	 * @param string $field
313
	 *
314
	 * @return mixed
315
	 */
316
	private function getUserValue(ORM\Mapping\ClassMetadata $classMetadata, $field)
317
	{
318
		$user = $this->getUser();
319
320
		if ($classMetadata->hasAssociation($field)) {
321
			if ($user !== NULL && ! is_object($user)) {
322
				throw new Exceptions\InvalidArgumentException("Blame is reference, user must be an object");
323
			}
324
325
			return $user;
326
		}
327
328
		// Ok so its not an association, then it is a string
329
		if (is_object($user)) {
330
			if (method_exists($user, '__toString')) {
331
				return $user->__toString();
332
			}
333
334
			throw new Exceptions\InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
335
		}
336
337
		return $user;
338
	}
339
340
	/**
341
	 * @param ORM\Mapping\ClassMetadata $classMetadata
342
	 * @param string $eventName
343
	 *
344
	 * @throws ORM\Mapping\MappingException
345
	 */
346
	private function registerEvent(ORM\Mapping\ClassMetadata $classMetadata, $eventName)
347
	{
348
		if (!$this->hasRegisteredListener($classMetadata, $eventName, get_called_class())) {
349
			$classMetadata->addEntityListener($eventName, get_called_class(), $eventName);
350
		}
351
	}
352
353
	/**
354
	 * @param ORM\Mapping\ClassMetadata $classMetadata
355
	 * @param string $eventName
356
	 * @param string $listenerClass
357
	 *
358
	 * @return bool
359
	 */
360
	private static function hasRegisteredListener(ORM\Mapping\ClassMetadata $classMetadata, $eventName, $listenerClass)
361
	{
362
		if (!isset($classMetadata->entityListeners[$eventName])) {
363
			return FALSE;
364
		}
365
366
		foreach ($classMetadata->entityListeners[$eventName] as $listener) {
367
			if ($listener['class'] === $listenerClass && $listener['method'] === $eventName) {
368
				return TRUE;
369
			}
370
		}
371
372
		return FALSE;
373
	}
374
}
375