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

Blameable::readExtendedMetadata()   D

Complexity

Conditions 28
Paths 107

Size

Total Lines 99
Code Lines 51

Duplication

Lines 12
Ratio 12.12 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 12
loc 99
rs 4.3938
cc 28
eloc 51
nc 107
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
{
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
		$this->objectManager = $objectManager;
85
		$this->configuration = $configuration;
86
	}
87
88
	/**
89
	 * @param ORM\Mapping\ClassMetadata $classMetadata
90
	 */
91
	public function loadMetadataForObjectClass(ORM\Mapping\ClassMetadata $classMetadata)
92
	{
93
		if ($classMetadata->isMappedSuperclass) {
94
			return; // Ignore mappedSuperclasses for now
95
		}
96
97
		// The annotation reader accepts a ReflectionClass, which can be
98
		// obtained from the $classMetadata
99
		$reflectionClass = $classMetadata->getReflectionClass();
100
101
		$config = [];
102
103
		$useObjectName = $classMetadata->getName();
104
105
		// Collect metadata from inherited classes
106
		if ($reflectionClass !== NULL) {
107
			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
			}
124
125
			$config = $this->readExtendedMetadata($classMetadata, $config);
126
		}
127
128
		if ($config !== []) {
129
			$config['useObjectClass'] = $useObjectName;
130
		}
131
132
		// Cache the metadata (even if it's empty)
133
		// Caching empty metadata will prevent re-parsing non-existent annotations
134
		$cacheId = self::getCacheId($classMetadata->getName());
135
136
		/** @var Common\Cache\Cache $cacheDriver */
137
		if ($cacheDriver = $this->objectManager->getMetadataFactory()->getCacheDriver()) {
138
			$cacheDriver->save($cacheId, $config, NULL);
139
		}
140
141
		self::$objectConfigurations[$classMetadata->getName()] = $config;
142
	}
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
		$class = $metadata->getReflectionClass();
156
157
		// Create doctrine annotation reader
158
		$reader = $this->getDefaultAnnotationReader();
159
160
		// Property annotations
161
		foreach ($class->getProperties() as $property) {
162
			if ($metadata->isMappedSuperclass && $property->isPrivate() === FALSE ||
163
				$metadata->isInheritedField($property->getName()) ||
164
				isset($metadata->associationMappings[$property->getName()]['inherited'])
165
			) {
166
				continue;
167
			}
168
169
			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...
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 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...
187
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
188
							}
189
190 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...
191
								$entityMap['joinColumns'][0]['referencedColumnName'] = $blameable->referencedColumnName;
192
							}
193
194
							$metadata->mapManyToOne($entityMap);
195
196
						} else if ($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)) {
213 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...
214
						throw new Exceptions\InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$metadata->getName()}");
215
					}
216
217
				} else if ($metadata->hasAssociation($field)) {
218
					// association
219 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...
220
						throw new Exceptions\InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$metadata->getName()}");
221
					}
222
				}
223
224
				// Check for valid events
225
				if (!in_array($blameable->on, ['update', 'create', 'change', 'delete'])) {
226
					throw new Exceptions\InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$metadata->getName()}");
227
				}
228
229
				if ($blameable->on === 'change') {
230
					if (!isset($blameable->field)) {
231
						throw new Exceptions\InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$metadata->getName()}");
232
					}
233
234
					if (is_array($blameable->field) && isset($blameable->value)) {
235
						throw new Exceptions\InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
236
					}
237
238
					$field = [
239
						'field'        => $field,
240
						'trackedField' => $blameable->field,
241
						'value'        => $blameable->value,
242
					];
243
				}
244
245
				// properties are unique and mapper checks that, no risk here
246
				$config[$blameable->on][] = $field;
247
			}
248
		}
249
250
		return $config;
251
	}
252
253
	/**
254
	 * Get the configuration for specific object class
255
	 * if cache driver is present it scans it also
256
	 *
257
	 * @param string $class
258
	 *
259
	 * @return array
260
	 */
261
	public function getObjectConfigurations($class)
262
	{
263
		$config = [];
264
265
		if (isset(self::$objectConfigurations[$class])) {
266
			$config = self::$objectConfigurations[$class];
267
268
		} else {
269
			$metadataFactory = $this->objectManager->getMetadataFactory();
270
			/** @var Common\Cache\Cache $cacheDriver|NULL */
271
			$cacheDriver = $metadataFactory->getCacheDriver();
272
273
			if ($cacheDriver !== NULL) {
274
				$cacheId = self::getCacheId($class);
275
276
				if (($cached = $cacheDriver->fetch($cacheId)) !== FALSE) {
277
					self::$objectConfigurations[$class] = $cached;
278
					$config = $cached;
279
280
				} else {
281
					/** @var ORM\Mapping\ClassMetadata $classMetadata */
282
					$classMetadata = $metadataFactory->getMetadataFor($class);
283
284
					// Re-generate metadata on cache miss
285
					$this->loadMetadataForObjectClass($classMetadata);
286
287
					if (isset(self::$objectConfigurations[$class])) {
288
						$config = self::$objectConfigurations[$class];
289
					}
290
				}
291
292
				$objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class;
293
294
				if ($objectClass !== $class) {
295
					$this->getObjectConfigurations($objectClass);
296
				}
297
			}
298
		}
299
300
		return $config;
301
	}
302
303
	/**
304
	 * Create default annotation reader for extensions
305
	 *
306
	 * @return Common\Annotations\AnnotationReader
307
	 */
308
	private function getDefaultAnnotationReader()
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 object $meta
325
	 * @param string $field
326
	 *
327
	 * @return boolean
328
	 */
329
	private function isValidField($meta, $field)
330
	{
331
		$mapping = $meta->getFieldMapping($field);
332
333
		return $mapping && in_array($mapping['type'], $this->validTypes);
334
	}
335
336
	/**
337
	 * Get the cache id
338
	 *
339
	 * @param string $className
340
	 *
341
	 * @return string
342
	 */
343
	private static function getCacheId($className)
344
	{
345
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
346
	}
347
348
}
349