Completed
Push — master ( 28f29f...4081af )
by Adam
08:00 queued 05:40
created

Blameable::loadMetadataForObjectClass()   C

Complexity

Conditions 10
Paths 9

Size

Total Lines 52
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 14.2071

Importance

Changes 0
Metric Value
dl 0
loc 52
ccs 15
cts 23
cp 0.6522
rs 6.2553
c 0
b 0
f 0
cc 10
eloc 23
nc 9
nop 2
crap 14.2071

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
declare(strict_types = 1);
16
17
namespace IPub\DoctrineBlameable\Mapping\Driver;
18
19
use Nette;
20
21
use Doctrine;
22
use Doctrine\Common;
23
use Doctrine\ORM;
24
25
use IPub;
26
use IPub\DoctrineBlameable;
27
use IPub\DoctrineBlameable\Exceptions;
28
use IPub\DoctrineBlameable\Mapping;
29
30
/**
31
 * Doctrine blameable annotation driver
32
 *
33
 * @package        iPublikuj:DoctrineBlameable!
34
 * @subpackage     Driver
35
 *
36
 * @author         Adam Kadlec <[email protected]>
37
 */
38 1
final class Blameable extends Nette\Object
39
{
40
	/**
41
	 * Define class name
42
	 */
43
	const CLASS_NAME = __CLASS__;
44
45
	/**
46
	 * Annotation field is blameable
47
	 */
48
	const EXTENSION_ANNOTATION = 'IPub\DoctrineBlameable\Mapping\Annotation\Blameable';
49
50
	/**
51
	 * @var DoctrineBlameable\Configuration
52
	 */
53
	private $configuration;
54
55
	/**
56
	 * List of cached object configurations
57
	 *
58
	 * @var array
59
	 */
60
	private static $objectConfigurations = [];
61
62
	/**
63
	 * List of types which are valid for blame
64
	 *
65
	 * @var array
66
	 */
67
	private $validTypes = [
68
		'one',
69
		'string',
70
		'int',
71
	];
72
73
	/**
74
	 * @param DoctrineBlameable\Configuration $configuration
75
	 */
76
	public function __construct(DoctrineBlameable\Configuration $configuration)
77
	{
78 1
		$this->configuration = $configuration;
79 1
	}
80
81
	/**
82
	 * @param Common\Persistence\ObjectManager $objectManager
83
	 * @param ORM\Mapping\ClassMetadata $classMetadata
84
	 *
85
	 * @return void
86
	 */
87
	public function loadMetadataForObjectClass(Common\Persistence\ObjectManager $objectManager, ORM\Mapping\ClassMetadata $classMetadata)
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();
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 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 Exceptions\InvalidMappingException
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)) {
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 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 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...
184
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
185
							}
186
187 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...
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
	public function getObjectConfigurations(Common\Persistence\ObjectManager $objectManager, string $class) : array
254
	{
255 1
		$config = [];
256
257 1
		if (isset(self::$objectConfigurations[$class])) {
258 1
			$config = self::$objectConfigurations[$class];
259
260
		} else {
261
			$metadataFactory = $objectManager->getMetadataFactory();
262
			/** @var Common\Cache\Cache $cacheDriver|NULL */
263
			$cacheDriver = $metadataFactory->getCacheDriver();
264
265
			if ($cacheDriver !== NULL) {
266
				$cacheId = self::getCacheId($class);
267
268
				if (($cached = $cacheDriver->fetch($cacheId)) !== FALSE) {
269
					self::$objectConfigurations[$class] = $cached;
270
					$config = $cached;
271
272
				} else {
273
					/** @var ORM\Mapping\ClassMetadata $classMetadata */
274
					$classMetadata = $metadataFactory->getMetadataFor($class);
275
276
					// Re-generate metadata on cache miss
277
					$this->loadMetadataForObjectClass($objectManager, $classMetadata);
278
279
					if (isset(self::$objectConfigurations[$class])) {
280
						$config = self::$objectConfigurations[$class];
281
					}
282
				}
283
284
				$objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class;
285
286
				if ($objectClass !== $class) {
287
					$this->getObjectConfigurations($objectManager, $objectClass);
288
				}
289
			}
290
		}
291
292 1
		return $config;
293
	}
294
295
	/**
296
	 * Create default annotation reader for extensions
297
	 *
298
	 * @return Common\Annotations\CachedReader
299
	 */
300
	private function getDefaultAnnotationReader() : Common\Annotations\CachedReader
301
	{
302 1
		$reader = new Common\Annotations\AnnotationReader;
303
304 1
		Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
305 1
			'IPub\\DoctrineBlameable\\Mapping\\Annotation'
306
		);
307
308 1
		$reader = new Common\Annotations\CachedReader($reader, new Common\Cache\ArrayCache);
309
310 1
		return $reader;
311
	}
312
313
	/**
314
	 * Checks if $field type is valid
315
	 *
316
	 * @param ORM\Mapping\ClassMetadata $meta
317
	 * @param string $field
318
	 *
319
	 * @return bool
320
	 */
321
	private function isValidField(ORM\Mapping\ClassMetadata $meta, string $field) : bool
322
	{
323 1
		$mapping = $meta->getFieldMapping($field);
324
325 1
		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...
326
	}
327
328
	/**
329
	 * Get the cache id
330
	 *
331
	 * @param string $className
332
	 *
333
	 * @return string
334
	 */
335
	private static function getCacheId(string $className) : string
336
	{
337 1
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
338
	}
339
340
}
341