Blameable::getDefaultAnnotationReader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
final class Blameable
37
{
38
	/**
39
	 * Implement nette smart magic
40
	 */
41
	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 Common\Annotations\AnnotationException
86
	 * @throws ORM\Mapping\MappingException
87
	 */
88
	public function loadMetadataForObjectClass(
89
		Common\Persistence\ObjectManager $objectManager,
90
		ORM\Mapping\ClassMetadata $classMetadata
91
	) : void {
92
		if ($classMetadata->isMappedSuperclass) {
93
			return; // Ignore mappedSuperclasses for now
94
		}
95
96
		// The annotation reader accepts a ReflectionClass, which can be
97
		// obtained from the $classMetadata
98
		$reflectionClass = $classMetadata->getReflectionClass();
99
100
		$config = [];
101
102
		$useObjectName = $classMetadata->getName();
103
104
		// Collect metadata from inherited classes
105
		if ($reflectionClass !== NULL) {
106
			foreach (array_reverse(class_parents($classMetadata->getName())) as $parentClass) {
107
				// Read only inherited mapped classes
108
				if ($objectManager->getMetadataFactory()->hasMetadataFor($parentClass)) {
109
					/** @var ORM\Mapping\ClassMetadata $parentClassMetadata */
110
					$parentClassMetadata = $objectManager->getClassMetadata($parentClass);
111
112
					$config = $this->readExtendedMetadata($parentClassMetadata, $config);
113
114
					$isBaseInheritanceLevel = !$parentClassMetadata->isInheritanceTypeNone()
115
						&& $parentClassMetadata->parentClasses !== []
116
						&& $config !== [];
117
118
					if ($isBaseInheritanceLevel === TRUE) {
119
						$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...
120
					}
121
				}
122
			}
123
124
			$config = $this->readExtendedMetadata($classMetadata, $config);
125
		}
126
127
		if ($config !== []) {
128
			$config['useObjectClass'] = $useObjectName;
129
		}
130
131
		// Cache the metadata (even if it's empty)
132
		// Caching empty metadata will prevent re-parsing non-existent annotations
133
		$cacheId = self::getCacheId($classMetadata->getName());
134
135
		/** @var Common\Cache\Cache $cacheDriver */
136
		if ($cacheDriver = $objectManager->getMetadataFactory()->getCacheDriver()) {
137
			$cacheDriver->save($cacheId, $config, NULL);
138
		}
139
140
		self::$objectConfigurations[$classMetadata->getName()] = $config;
141
	}
142
143
	/**
144
	 * @param ORM\Mapping\ClassMetadata $metadata
145
	 * @param array $config
146
	 *
147
	 * @return array
148
	 *
149
	 * @throws Common\Annotations\AnnotationException
150
	 * @throws ORM\Mapping\MappingException
151
	 */
152
	private function readExtendedMetadata(ORM\Mapping\ClassMetadata $metadata, array $config) : array
153
	{
154
		$class = $metadata->getReflectionClass();
155
156
		// Create doctrine annotation reader
157
		$reader = $this->getDefaultAnnotationReader();
158
159
		// Property annotations
160
		foreach ($class->getProperties() as $property) {
161
			if ($metadata->isMappedSuperclass && $property->isPrivate() === FALSE ||
162
				$metadata->isInheritedField($property->getName()) ||
163
				isset($metadata->associationMappings[$property->getName()]['inherited'])
164
			) {
165
				continue;
166
			}
167
168
			/** @var Mapping\Annotation\Blameable $blameable */
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
							if (isset($blameable->association['column']) && $blameable->association['column'] !== NULL) {
187
								$entityMap['joinColumns'][0]['name'] = $blameable->columnName;
188
							}
189
190
							if (isset($blameable->association['referencedColumn']) && $blameable->association['referencedColumn'] !== NULL) {
191
								$entityMap['joinColumns'][0]['referencedColumnName'] = $blameable->referencedColumnName;
192
							}
193
194
							$metadata->mapManyToOne($entityMap);
195
196
						} elseif ($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) && $this->isValidField($metadata, $field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
213
					throw new Exceptions\InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$metadata->getName()}");
214
215
				} elseif ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field) === FALSE && $this->configuration->useLazyAssociation() === FALSE) {
216
					throw new Exceptions\InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$metadata->getName()}");
217
				}
218
219
				// Check for valid events
220
				if (!in_array($blameable->on, ['update', 'create', 'change', 'delete'])) {
221
					throw new Exceptions\InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$metadata->getName()}");
222
				}
223
224
				if ($blameable->on === 'change') {
225
					if (!isset($blameable->field)) {
226
						throw new Exceptions\InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$metadata->getName()}");
227
					}
228
229
					if (is_array($blameable->field) && isset($blameable->value)) {
230
						throw new Exceptions\InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
231
					}
232
233
					$field = [
234
						'field'        => $field,
235
						'trackedField' => $blameable->field,
236
						'value'        => is_array($blameable->value) ? $blameable->value : [$blameable->value],
237
					];
238
				}
239
240
				$config[$blameable->on][] = $field;
241
			}
242
		}
243
244
		return $config;
245
	}
246
247
	/**
248
	 * Get the configuration for specific object class
249
	 * if cache driver is present it scans it also
250
	 *
251
	 * @param Common\Persistence\ObjectManager $objectManager
252
	 * @param string $class
253
	 *
254
	 * @return array
255
	 *
256
	 * @throws Common\Annotations\AnnotationException
257
	 * @throws ORM\Mapping\MappingException
258
	 */
259
	public function getObjectConfigurations(Common\Persistence\ObjectManager $objectManager, string $class) : array
260
	{
261
		$config = [];
262
263
		if (isset(self::$objectConfigurations[$class])) {
264
			$config = self::$objectConfigurations[$class];
265
266
		} else {
267
			$metadataFactory = $objectManager->getMetadataFactory();
268
			/** @var Common\Cache\Cache $cacheDriver |NULL */
269
			$cacheDriver = $metadataFactory->getCacheDriver();
270
271
			if ($cacheDriver !== NULL) {
272
				$cacheId = self::getCacheId($class);
273
274
				if (($cached = $cacheDriver->fetch($cacheId)) !== FALSE) {
275
					self::$objectConfigurations[$class] = $cached;
276
					$config = $cached;
277
278
				} else {
279
					/** @var ORM\Mapping\ClassMetadata $classMetadata */
280
					$classMetadata = $metadataFactory->getMetadataFor($class);
281
282
					// Re-generate metadata on cache miss
283
					$this->loadMetadataForObjectClass($objectManager, $classMetadata);
284
285
					if (isset(self::$objectConfigurations[$class])) {
286
						$config = self::$objectConfigurations[$class];
287
					}
288
				}
289
290
				$objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class;
291
292
				if ($objectClass !== $class) {
293
					$this->getObjectConfigurations($objectManager, $objectClass);
294
				}
295
			}
296
		}
297
298
		return $config;
299
	}
300
301
	/**
302
	 * Create default annotation reader for extensions
303
	 *
304
	 * @return Common\Annotations\CachedReader
305
	 *
306
	 * @throws Common\Annotations\AnnotationException
307
	 */
308
	private function getDefaultAnnotationReader() : Common\Annotations\CachedReader
309
	{
310
		$reader = new Common\Annotations\AnnotationReader;
311
312
		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. Annotations will be autoloaded in 2.0.

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...
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 ORM\Mapping\ClassMetadata $meta
325
	 * @param string $field
326
	 *
327
	 * @return bool
328
	 *
329
	 * @throws ORM\Mapping\MappingException
330
	 */
331
	private function isValidField(ORM\Mapping\ClassMetadata $meta, string $field) : bool
332
	{
333
		$mapping = $meta->getFieldMapping($field);
334
335
		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...
336
	}
337
338
	/**
339
	 * Get the cache id
340
	 *
341
	 * @param string $className
342
	 *
343
	 * @return string
344
	 */
345
	private static function getCacheId(string $className) : string
346
	{
347
		return $className . '\\$' . strtoupper(str_replace('\\', '_', __NAMESPACE__)) . '_CLASSMETADATA';
348
	}
349
350
}
351