Passed
Push — master ( 7b5b50...9096f6 )
by Adam
01:59
created

Blameable::readExtendedMetadata()   D

Complexity

Conditions 29
Paths 53

Size

Total Lines 94

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 37.894

Importance

Changes 0
Metric Value
dl 0
loc 94
ccs 32
cts 41
cp 0.7805
rs 4.1666
c 0
b 0
f 0
cc 29
nc 53
nop 2
crap 37.894

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        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 1
		$this->configuration = $configuration;
77 1
	}
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 1
		if ($classMetadata->isMappedSuperclass) {
90 1
			return; // Ignore mappedSuperclasses for now
91
		}
92
93
		// The annotation reader accepts a ReflectionClass, which can be
94
		// obtained from the $classMetadata
95 1
		$reflectionClass = $classMetadata->getReflectionClass();
96
97 1
		$config = [];
98
99 1
		$useObjectName = $classMetadata->getName();
100
101
		// Collect metadata from inherited classes
102 1
		if ($reflectionClass !== NULL) {
103 1
			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();
117
					}
118
				}
119
			}
120
121 1
			$config = $this->readExtendedMetadata($classMetadata, $config);
122
		}
123
124 1
		if ($config !== []) {
125 1
			$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 1
		$cacheId = self::getCacheId($classMetadata->getName());
131
132
		/** @var Common\Cache\Cache $cacheDriver */
133 1
		if ($cacheDriver = $objectManager->getMetadataFactory()->getCacheDriver()) {
134 1
			$cacheDriver->save($cacheId, $config, NULL);
135
		}
136
137 1
		self::$objectConfigurations[$classMetadata->getName()] = $config;
138 1
	}
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 1
		$class = $metadata->getReflectionClass();
152
153
		// Create doctrine annotation reader
154 1
		$reader = $this->getDefaultAnnotationReader();
155
156
		// Property annotations
157 1
		foreach ($class->getProperties() as $property) {
158 1
			if ($metadata->isMappedSuperclass && $property->isPrivate() === FALSE ||
159 1
				$metadata->isInheritedField($property->getName()) ||
160 1
				isset($metadata->associationMappings[$property->getName()]['inherited'])
161
			) {
162
				continue;
163
			}
164
165
			/** @var Mapping\Annotation\Blameable $blameable */
166 1
			if ($blameable = $reader->getPropertyAnnotation($property, self::EXTENSION_ANNOTATION)) {
167 1
				$field = $property->getName();
168
169
				// No map field nor association
170 1
				if ($metadata->hasField($field) === FALSE && $metadata->hasAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
171 1
					if ($this->configuration->automapField) {
172 1
						if ($this->configuration->automapWithAssociation()) {
173
							$entityMap = [
174 1
								'targetEntity' => $this->configuration->userEntity,
175 1
								'fieldName'    => $field,
176
								'joinColumns'  => [
177
									[
178
										'onDelete' => 'SET NULL',
179
									]
180
								]
181
							];
182
183 1
							if (isset($blameable->association['column']) && $blameable->association['column'] !== NULL) {
184
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
185
							}
186
187 1
							if (isset($blameable->association['referencedColumn']) && $blameable->association['referencedColumn'] !== NULL) {
188
								$entityMap['joinColumns'][0]['referencedColumnName'] = $blameable->referencedColumnName;
189
							}
190
191 1
							$metadata->mapManyToOne($entityMap);
192
193 1
						} else if ($this->configuration->automapWithField()) {
194 1
							$metadata->mapField([
195 1
								'fieldName' => $field,
196 1
								'type'      => 'string',
197
								'nullable'  => TRUE,
198
							]);
199
200
						} else {
201 1
							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 1
				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 1
				} 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 1
				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 1
				if ($blameable->on === 'change') {
222 1
					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 1
					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 1
						'field'        => $field,
232 1
						'trackedField' => $blameable->field,
233 1
						'value'        => is_array($blameable->value) ? $blameable->value : [$blameable->value],
234
					];
235
				}
236
237 1
				$config[$blameable->on][] = $field;
238
			}
239
		}
240
241 1
		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 1
		$config = [];
258
259 1
		if (isset(self::$objectConfigurations[$class])) {
260 1
			$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 1
		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 1
		$reader = new Common\Annotations\AnnotationReader;
307
308 1
		Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
309 1
			'IPub\\DoctrineBlameable\\Mapping\\Annotation'
310
		);
311
312 1
		$reader = new Common\Annotations\CachedReader($reader, new Common\Cache\ArrayCache);
313
314 1
		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 1
		$mapping = $meta->getFieldMapping($field);
330
331 1
		return $mapping && in_array($mapping['type'], $this->validTypes);
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 1
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
344
	}
345
346
}
347