Test Failed
Push — master ( aaf2c8...6e983e )
by Adam
06:42
created

Blameable   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 2.13%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 11
dl 0
loc 311
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 52 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 ORM\Mapping\MappingException
86
	 */
87
	public function loadMetadataForObjectClass(Common\Persistence\ObjectManager $objectManager, ORM\Mapping\ClassMetadata $classMetadata) : void
88
	{
89
		if ($classMetadata->isMappedSuperclass) {
90
			return; // Ignore mappedSuperclasses for now
91
		}
92
93
		// The annotation reader accepts a ReflectionClass, which can be
94
		// obtained from the $classMetadata
95
		$reflectionClass = $classMetadata->getReflectionClass();
96
97
		$config = [];
98
99
		$useObjectName = $classMetadata->getName();
100
101
		// Collect metadata from inherited classes
102
		if ($reflectionClass !== NULL) {
103
			foreach (array_reverse(class_parents($classMetadata->getName())) as $parentClass) {
104
				// Read only inherited mapped classes
105
				if ($objectManager->getMetadataFactory()->hasMetadataFor($parentClass)) {
106
					/** @var ORM\Mapping\ClassMetadata $parentClassMetadata */
107
					$parentClassMetadata = $objectManager->getClassMetadata($parentClass);
108
109
					$config = $this->readExtendedMetadata($parentClassMetadata, $config);
110
111
					$isBaseInheritanceLevel = !$parentClassMetadata->isInheritanceTypeNone()
112
						&& $parentClassMetadata->parentClasses !== []
113
						&& $config !== [];
114
115
					if ($isBaseInheritanceLevel === TRUE) {
116
						$useObjectName = $reflectionClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
117
					}
118
				}
119
			}
120
121
			$config = $this->readExtendedMetadata($classMetadata, $config);
122
		}
123
124
		if ($config !== []) {
125
			$config['useObjectClass'] = $useObjectName;
126
		}
127
128
		// Cache the metadata (even if it's empty)
129
		// Caching empty metadata will prevent re-parsing non-existent annotations
130
		$cacheId = self::getCacheId($classMetadata->getName());
131
132
		/** @var Common\Cache\Cache $cacheDriver */
133
		if ($cacheDriver = $objectManager->getMetadataFactory()->getCacheDriver()) {
134
			$cacheDriver->save($cacheId, $config, NULL);
135
		}
136
137
		self::$objectConfigurations[$classMetadata->getName()] = $config;
138
	}
139
140
	/**
141
	 * @param ORM\Mapping\ClassMetadata $metadata
142
	 * @param array $config
143
	 *
144
	 * @return array
145
	 *
146
	 * @throws Common\Annotations\AnnotationException
147
	 * @throws ORM\Mapping\MappingException
148
	 */
149
	private function readExtendedMetadata(ORM\Mapping\ClassMetadata $metadata, array $config) : array
150
	{
151
		$class = $metadata->getReflectionClass();
152
153
		// Create doctrine annotation reader
154
		$reader = $this->getDefaultAnnotationReader();
155
156
		// Property annotations
157
		foreach ($class->getProperties() as $property) {
158
			if ($metadata->isMappedSuperclass && $property->isPrivate() === FALSE ||
159
				$metadata->isInheritedField($property->getName()) ||
160
				isset($metadata->associationMappings[$property->getName()]['inherited'])
161
			) {
162
				continue;
163
			}
164
165
			/** @var Mapping\Annotation\Blameable $blameable */
166
			if ($blameable = $reader->getPropertyAnnotation($property, self::EXTENSION_ANNOTATION)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $blameable is correct as $reader->getPropertyAnno...::EXTENSION_ANNOTATION) (which targets Doctrine\Common\Annotati...getPropertyAnnotation()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
167
				$field = $property->getName();
168
169
				// No map field nor association
170
				if ($metadata->hasField($field) === FALSE && $metadata->hasAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
171
					if ($this->configuration->automapField) {
172
						if ($this->configuration->automapWithAssociation()) {
173
							$entityMap = [
174
								'targetEntity' => $this->configuration->userEntity,
175
								'fieldName'    => $field,
176
								'joinColumns'  => [
177
									[
178
										'onDelete' => 'SET NULL',
179
									]
180
								]
181
							];
182
183
							if (isset($blameable->association['column']) && $blameable->association['column'] !== NULL) {
184
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
185
							}
186
187
							if (isset($blameable->association['referencedColumn']) && $blameable->association['referencedColumn'] !== NULL) {
188
								$entityMap['joinColumns'][0]['referencedColumnName'] = $blameable->referencedColumnName;
189
							}
190
191
							$metadata->mapManyToOne($entityMap);
192
193
						} else if ($this->configuration->automapWithField()) {
194
							$metadata->mapField([
195
								'fieldName' => $field,
196
								'type'      => 'string',
197
								'nullable'  => TRUE,
198
							]);
199
200
						} else {
201
							throw new Exceptions\InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$metadata->getName()}");
202
						}
203
204
					} else {
205
						throw new Exceptions\InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$metadata->getName()}");
206
					}
207
				}
208
209
				if ($metadata->hasField($field) && $this->isValidField($metadata, $field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
210
					throw new Exceptions\InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$metadata->getName()}");
211
212
				} else if ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
213
					throw new Exceptions\InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$metadata->getName()}");
214
				}
215
216
				// Check for valid events
217
				if (!in_array($blameable->on, ['update', 'create', 'change', 'delete'])) {
218
					throw new Exceptions\InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$metadata->getName()}");
219
				}
220
221
				if ($blameable->on === 'change') {
222
					if (!isset($blameable->field)) {
223
						throw new Exceptions\InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$metadata->getName()}");
224
					}
225
226
					if (is_array($blameable->field) && isset($blameable->value)) {
227
						throw new Exceptions\InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
228
					}
229
230
					$field = [
231
						'field'        => $field,
232
						'trackedField' => $blameable->field,
233
						'value'        => is_array($blameable->value) ? $blameable->value : [$blameable->value],
234
					];
235
				}
236
237
				$config[$blameable->on][] = $field;
238
			}
239
		}
240
241
		return $config;
242
	}
243
244
	/**
245
	 * Get the configuration for specific object class
246
	 * if cache driver is present it scans it also
247
	 *
248
	 * @param Common\Persistence\ObjectManager $objectManager
249
	 * @param string $class
250
	 *
251
	 * @return array
252
	 *
253
	 * @throws ORM\Mapping\MappingException
254
	 */
255
	public function getObjectConfigurations(Common\Persistence\ObjectManager $objectManager, string $class) : array
256
	{
257
		$config = [];
258
259
		if (isset(self::$objectConfigurations[$class])) {
260
			$config = self::$objectConfigurations[$class];
261
262
		} else {
263
			$metadataFactory = $objectManager->getMetadataFactory();
264
			/** @var Common\Cache\Cache $cacheDriver |NULL */
265
			$cacheDriver = $metadataFactory->getCacheDriver();
266
267
			if ($cacheDriver !== NULL) {
268
				$cacheId = self::getCacheId($class);
269
270
				if (($cached = $cacheDriver->fetch($cacheId)) !== FALSE) {
271
					self::$objectConfigurations[$class] = $cached;
272
					$config = $cached;
273
274
				} else {
275
					/** @var ORM\Mapping\ClassMetadata $classMetadata */
276
					$classMetadata = $metadataFactory->getMetadataFor($class);
277
278
					// Re-generate metadata on cache miss
279
					$this->loadMetadataForObjectClass($objectManager, $classMetadata);
280
281
					if (isset(self::$objectConfigurations[$class])) {
282
						$config = self::$objectConfigurations[$class];
283
					}
284
				}
285
286
				$objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class;
287
288
				if ($objectClass !== $class) {
289
					$this->getObjectConfigurations($objectManager, $objectClass);
290
				}
291
			}
292
		}
293
294
		return $config;
295
	}
296
297
	/**
298
	 * Create default annotation reader for extensions
299
	 *
300
	 * @return Common\Annotations\CachedReader
301
	 *
302
	 * @throws Common\Annotations\AnnotationException
303
	 */
304
	private function getDefaultAnnotationReader() : Common\Annotations\CachedReader
305
	{
306
		$reader = new Common\Annotations\AnnotationReader;
307
308
		Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...sterAutoloadNamespace() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
309
			'IPub\\DoctrineBlameable\\Mapping\\Annotation'
310
		);
311
312
		$reader = new Common\Annotations\CachedReader($reader, new Common\Cache\ArrayCache);
313
314
		return $reader;
315
	}
316
317
	/**
318
	 * Checks if $field type is valid
319
	 *
320
	 * @param ORM\Mapping\ClassMetadata $meta
321
	 * @param string $field
322
	 *
323
	 * @return bool
324
	 *
325
	 * @throws ORM\Mapping\MappingException
326
	 */
327
	private function isValidField(ORM\Mapping\ClassMetadata $meta, string $field) : bool
328
	{
329
		$mapping = $meta->getFieldMapping($field);
330
331
		return $mapping && in_array($mapping['type'], $this->validTypes);
0 ignored issues
show
Bug Best Practice introduced by
The expression $mapping of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
332
	}
333
334
	/**
335
	 * Get the cache id
336
	 *
337
	 * @param string $className
338
	 *
339
	 * @return string
340
	 */
341
	private static function getCacheId(string $className) : string
342
	{
343
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
344
	}
345
346
}
347