Completed
Push — master ( 28f418...2fc84b )
by Adam
19:18 queued 10:19
created

Blameable   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 314
Duplicated Lines 3.82 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 67.48%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 12
loc 314
ccs 83
cts 123
cp 0.6748
wmc 50
lcom 1
cbo 10
rs 8.6207

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
C loadMetadataForObjectClass() 0 52 10
C getObjectConfigurations() 0 41 7
A getDefaultAnnotationReader() 0 12 1
A isValidField() 0 6 2
A getCacheId() 0 4 1
D readExtendedMetadata() 12 100 28

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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