Test Failed
Pull Request — master (#3)
by Adam
01:47
created

Blameable   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 2.13%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 2
dl 0
loc 315
ccs 2
cts 94
cp 0.0213
rs 7.92
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B loadMetadataForObjectClass() 0 54 10
D readExtendedMetadata() 0 94 29
B getObjectConfigurations() 0 41 7
A getDefaultAnnotationReader() 0 12 1
A isValidField() 0 6 2
A getCacheId() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Blameable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Blameable, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Blameable.php
4
 *
5
 * @copyright      More in license.md
6
 * @license        https://www.ipublikuj.eu
7
 * @author         Adam Kadlec <[email protected]>
8
 * @package        iPublikuj:DoctrineBlameable!
9
 * @subpackage     Driver
10
 * @since          1.0.0
11
 *
12
 * @date           05.01.16
13
 */
14
15
declare(strict_types = 1);
16
17
namespace IPub\DoctrineBlameable\Mapping\Driver;
18
19
use Nette;
20
21
use Doctrine\Common;
22
use Doctrine\ORM;
23
24
use IPub\DoctrineBlameable;
25
use IPub\DoctrineBlameable\Exceptions;
26
use IPub\DoctrineBlameable\Mapping;
27
28
/**
29
 * Doctrine blameable annotation driver
30
 *
31
 * @package        iPublikuj:DoctrineBlameable!
32
 * @subpackage     Driver
33
 *
34
 * @author         Adam Kadlec <[email protected]>
35
 */
36 1
final class Blameable
37
{
38
	/**
39
	 * Implement nette smart magic
40
	 */
41 1
	use Nette\SmartObject;
42
43
	/**
44
	 * Annotation field is blameable
45
	 */
46
	private const EXTENSION_ANNOTATION = 'IPub\DoctrineBlameable\Mapping\Annotation\Blameable';
47
48
	/**
49
	 * @var DoctrineBlameable\Configuration
50
	 */
51
	private $configuration;
52
53
	/**
54
	 * List of cached object configurations
55
	 *
56
	 * @var array
57
	 */
58
	private static $objectConfigurations = [];
59
60
	/**
61
	 * List of types which are valid for blame
62
	 *
63
	 * @var array
64
	 */
65
	private $validTypes = [
66
		'one',
67
		'string',
68
		'int',
69
	];
70
71
	/**
72
	 * @param DoctrineBlameable\Configuration $configuration
73
	 */
74
	public function __construct(DoctrineBlameable\Configuration $configuration)
75
	{
76
		$this->configuration = $configuration;
77
	}
78
79
	/**
80
	 * @param Common\Persistence\ObjectManager $objectManager
81
	 * @param ORM\Mapping\ClassMetadata $classMetadata
82
	 *
83
	 * @return void
84
	 *
85
	 * @throws Common\Annotations\AnnotationException
86
	 * @throws ORM\Mapping\MappingException
87
	 */
88
	public function loadMetadataForObjectClass(
89
		Common\Persistence\ObjectManager $objectManager,
90
		ORM\Mapping\ClassMetadata $classMetadata
91
	) : void {
92
		if ($classMetadata->isMappedSuperclass) {
93
			return; // Ignore mappedSuperclasses for now
94
		}
95
96
		// The annotation reader accepts a ReflectionClass, which can be
97
		// obtained from the $classMetadata
98
		$reflectionClass = $classMetadata->getReflectionClass();
99
100
		$config = [];
101
102
		$useObjectName = $classMetadata->getName();
103
104
		// Collect metadata from inherited classes
105
		if ($reflectionClass !== NULL) {
106
			foreach (array_reverse(class_parents($classMetadata->getName())) as $parentClass) {
107
				// Read only inherited mapped classes
108
				if ($objectManager->getMetadataFactory()->hasMetadataFor($parentClass)) {
109
					/** @var ORM\Mapping\ClassMetadata $parentClassMetadata */
110
					$parentClassMetadata = $objectManager->getClassMetadata($parentClass);
111
112
					$config = $this->readExtendedMetadata($parentClassMetadata, $config);
113
114
					$isBaseInheritanceLevel = !$parentClassMetadata->isInheritanceTypeNone()
115
						&& $parentClassMetadata->parentClasses !== []
116
						&& $config !== [];
117
118
					if ($isBaseInheritanceLevel === TRUE) {
119
						$useObjectName = $reflectionClass->getName();
120
					}
121
				}
122
			}
123
124
			$config = $this->readExtendedMetadata($classMetadata, $config);
125
		}
126
127
		if ($config !== []) {
128
			$config['useObjectClass'] = $useObjectName;
129
		}
130
131
		// Cache the metadata (even if it's empty)
132
		// Caching empty metadata will prevent re-parsing non-existent annotations
133
		$cacheId = self::getCacheId($classMetadata->getName());
134
135
		/** @var Common\Cache\Cache $cacheDriver */
136
		if ($cacheDriver = $objectManager->getMetadataFactory()->getCacheDriver()) {
137
			$cacheDriver->save($cacheId, $config, NULL);
138
		}
139
140
		self::$objectConfigurations[$classMetadata->getName()] = $config;
141
	}
142
143
	/**
144
	 * @param ORM\Mapping\ClassMetadata $metadata
145
	 * @param array $config
146
	 *
147
	 * @return array
148
	 *
149
	 * @throws Common\Annotations\AnnotationException
150
	 * @throws ORM\Mapping\MappingException
151
	 */
152
	private function readExtendedMetadata(ORM\Mapping\ClassMetadata $metadata, array $config) : array
153
	{
154
		$class = $metadata->getReflectionClass();
155
156
		// Create doctrine annotation reader
157
		$reader = $this->getDefaultAnnotationReader();
158
159
		// Property annotations
160
		foreach ($class->getProperties() as $property) {
161
			if ($metadata->isMappedSuperclass && $property->isPrivate() === FALSE ||
162
				$metadata->isInheritedField($property->getName()) ||
163
				isset($metadata->associationMappings[$property->getName()]['inherited'])
164
			) {
165
				continue;
166
			}
167
168
			/** @var Mapping\Annotation\Blameable $blameable */
169
			if ($blameable = $reader->getPropertyAnnotation($property, self::EXTENSION_ANNOTATION)) {
170
				$field = $property->getName();
171
172
				// No map field nor association
173
				if ($metadata->hasField($field) === FALSE && $metadata->hasAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
174
					if ($this->configuration->automapField) {
175
						if ($this->configuration->automapWithAssociation()) {
176
							$entityMap = [
177
								'targetEntity' => $this->configuration->userEntity,
178
								'fieldName'    => $field,
179
								'joinColumns'  => [
180
									[
181
										'onDelete' => 'SET NULL',
182
									],
183
								],
184
							];
185
186
							if (isset($blameable->association['column']) && $blameable->association['column'] !== NULL) {
187
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
188
							}
189
190
							if (isset($blameable->association['referencedColumn']) && $blameable->association['referencedColumn'] !== NULL) {
191
								$entityMap['joinColumns'][0]['referencedColumnName'] = $blameable->referencedColumnName;
192
							}
193
194
							$metadata->mapManyToOne($entityMap);
195
196
						} elseif ($this->configuration->automapWithField()) {
197
							$metadata->mapField([
198
								'fieldName' => $field,
199
								'type'      => 'string',
200
								'nullable'  => TRUE,
201
							]);
202
203
						} else {
204
							throw new Exceptions\InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$metadata->getName()}");
205
						}
206
207
					} else {
208
						throw new Exceptions\InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$metadata->getName()}");
209
					}
210
				}
211
212
				if ($metadata->hasField($field) && $this->isValidField($metadata, $field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
213
					throw new Exceptions\InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$metadata->getName()}");
214
215
				} elseif ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
216
					throw new Exceptions\InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$metadata->getName()}");
217
				}
218
219
				// Check for valid events
220
				if (!in_array($blameable->on, ['update', 'create', 'change', 'delete'])) {
221
					throw new Exceptions\InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$metadata->getName()}");
222
				}
223
224
				if ($blameable->on === 'change') {
225
					if (!isset($blameable->field)) {
226
						throw new Exceptions\InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$metadata->getName()}");
227
					}
228
229
					if (is_array($blameable->field) && isset($blameable->value)) {
230
						throw new Exceptions\InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
231
					}
232
233
					$field = [
234
						'field'        => $field,
235
						'trackedField' => $blameable->field,
236
						'value'        => is_array($blameable->value) ? $blameable->value : [$blameable->value],
237
					];
238
				}
239
240
				$config[$blameable->on][] = $field;
241
			}
242
		}
243
244
		return $config;
245
	}
246
247
	/**
248
	 * Get the configuration for specific object class
249
	 * if cache driver is present it scans it also
250
	 *
251
	 * @param Common\Persistence\ObjectManager $objectManager
252
	 * @param string $class
253
	 *
254
	 * @return array
255
	 *
256
	 * @throws Common\Annotations\AnnotationException
257
	 * @throws ORM\Mapping\MappingException
258
	 */
259
	public function getObjectConfigurations(Common\Persistence\ObjectManager $objectManager, string $class) : array
260
	{
261
		$config = [];
262
263
		if (isset(self::$objectConfigurations[$class])) {
264
			$config = self::$objectConfigurations[$class];
265
266
		} else {
267
			$metadataFactory = $objectManager->getMetadataFactory();
268
			/** @var Common\Cache\Cache $cacheDriver |NULL */
269
			$cacheDriver = $metadataFactory->getCacheDriver();
270
271
			if ($cacheDriver !== NULL) {
272
				$cacheId = self::getCacheId($class);
273
274
				if (($cached = $cacheDriver->fetch($cacheId)) !== FALSE) {
275
					self::$objectConfigurations[$class] = $cached;
276
					$config = $cached;
277
278
				} else {
279
					/** @var ORM\Mapping\ClassMetadata $classMetadata */
280
					$classMetadata = $metadataFactory->getMetadataFor($class);
281
282
					// Re-generate metadata on cache miss
283
					$this->loadMetadataForObjectClass($objectManager, $classMetadata);
284
285
					if (isset(self::$objectConfigurations[$class])) {
286
						$config = self::$objectConfigurations[$class];
287
					}
288
				}
289
290
				$objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class;
291
292
				if ($objectClass !== $class) {
293
					$this->getObjectConfigurations($objectManager, $objectClass);
294
				}
295
			}
296
		}
297
298
		return $config;
299
	}
300
301
	/**
302
	 * Create default annotation reader for extensions
303
	 *
304
	 * @return Common\Annotations\CachedReader
305
	 *
306
	 * @throws Common\Annotations\AnnotationException
307
	 */
308
	private function getDefaultAnnotationReader() : Common\Annotations\CachedReader
309
	{
310
		$reader = new Common\Annotations\AnnotationReader;
311
312
		Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
313
			'IPub\\DoctrineBlameable\\Mapping\\Annotation'
314
		);
315
316
		$reader = new Common\Annotations\CachedReader($reader, new Common\Cache\ArrayCache);
317
318
		return $reader;
319
	}
320
321
	/**
322
	 * Checks if $field type is valid
323
	 *
324
	 * @param ORM\Mapping\ClassMetadata $meta
325
	 * @param string $field
326
	 *
327
	 * @return bool
328
	 *
329
	 * @throws ORM\Mapping\MappingException
330
	 */
331
	private function isValidField(ORM\Mapping\ClassMetadata $meta, string $field) : bool
332
	{
333
		$mapping = $meta->getFieldMapping($field);
334
335
		return $mapping && in_array($mapping['type'], $this->validTypes);
336
	}
337
338
	/**
339
	 * Get the cache id
340
	 *
341
	 * @param string $className
342
	 *
343
	 * @return string
344
	 */
345
	private static function getCacheId(string $className) : string
346
	{
347
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
348
	}
349
350
}
351