vendor/spatie/data-transfer-object/src/FieldValidator.php line 49

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Spatie\DataTransferObject;
  4. use ReflectionClass;
  5. use ReflectionProperty;
  6. abstract class FieldValidator
  7. {
  8. public bool $isNullable;
  9. public bool $isMixed;
  10. public bool $isMixedArray;
  11. public bool $hasDefaultValue;
  12. public array $allowedTypes;
  13. public array $allowedArrayTypes;
  14. public array $allowedArrayKeyTypes;
  15. protected static array $typeMapping = [
  16. 'int' => 'integer',
  17. 'bool' => 'boolean',
  18. 'float' => 'double',
  19. ];
  20. protected bool $hasTypeDeclaration;
  21. public static function fromReflection(ReflectionProperty $property): FieldValidator
  22. {
  23. $docDefinition = null;
  24. if ($property->getDocComment()) {
  25. preg_match(
  26. DocblockFieldValidator::DOCBLOCK_REGEX,
  27. $property->getDocComment(),
  28. $matches
  29. );
  30. $docDefinition = $matches[0] ?? null;
  31. }
  32. if ($docDefinition) {
  33. return new DocblockFieldValidator($docDefinition, $property->isDefault());
  34. }
  35. return new PropertyFieldValidator($property);
  36. }
  37. public function isValidType($value): bool
  38. {
  39. if (! $this->hasTypeDeclaration) {
  40. return true;
  41. }
  42. if ($this->isMixed) {
  43. return true;
  44. }
  45. if (is_iterable($value) && $this->isMixedArray) {
  46. return true;
  47. }
  48. if ($this->isNullable && $value === null) {
  49. return true;
  50. }
  51. if (is_iterable($value)) {
  52. return $this->isValidIterable($value);
  53. }
  54. return $this->isValidValue($value);
  55. }
  56. private function isValidIterable(iterable $iterable): bool
  57. {
  58. // If the iterable matches one of the normal types, we immediately return true
  59. // For example: custom collection classes type hinted with `MyCollection`
  60. $isValidValue = $this->isValidValue($iterable);
  61. if ($isValidValue) {
  62. return true;
  63. }
  64. // If not, we'll check all individual iterable items and keys
  65. foreach ($iterable as $key => $value) {
  66. $isValidValue = false;
  67. // First we check whether the value matches the value type definition
  68. foreach ($this->allowedArrayTypes as $type) {
  69. $isValidValue = $this->assertValidType($type, $value);
  70. // No need to further check this value when a valid type is found
  71. if ($isValidValue) {
  72. break;
  73. }
  74. }
  75. // If a value is invalid, we immediately return false
  76. if (! $isValidValue) {
  77. return false;
  78. }
  79. // We'll assume keys are valid by default, since they can be omitted
  80. $isValidKey = true;
  81. // Next we check the key's value
  82. foreach ($this->allowedArrayKeyTypes as $keyType) {
  83. $isValidKey = $this->assertValidType($keyType, $key);
  84. // No need to further check this jey when a valid type is found
  85. if ($isValidKey) {
  86. break;
  87. }
  88. }
  89. // If a key type is invalid, we'll immediately return
  90. if (! $isValidKey) {
  91. return false;
  92. }
  93. // Moving on to checking the next $key => $value pair
  94. }
  95. // If value and key type checks pass, we can return true
  96. return true;
  97. }
  98. private function isValidValue($value): bool
  99. {
  100. foreach ($this->allowedTypes as $type) {
  101. // We'll check the type of this value against all allowed types, if one matches we're good
  102. $isValidType = $this->assertValidType($type, $value);
  103. if ($isValidType) {
  104. return true;
  105. }
  106. }
  107. return false;
  108. }
  109. private function assertValidType(string $type, $value): bool
  110. {
  111. return $value instanceof $type || gettype($value) === $type;
  112. }
  113. protected function resolveAllowedArrayTypesFromCollection(string $type): array
  114. {
  115. if (! is_subclass_of($type, DataTransferObjectCollection::class)) {
  116. return [];
  117. }
  118. $class = new ReflectionClass($type);
  119. $currentReturnType = $class->getMethod('current')->getReturnType();
  120. // We cast to array to support future union types in PHP 8
  121. $currentReturnTypes = [];
  122. if ($currentReturnType) {
  123. $currentReturnTypes[] = $currentReturnType->getName();
  124. }
  125. $docblockReturnTypes = $class->getDocComment()
  126. ? $this->getCurrentReturnTypesFromDocblock($class->getDocComment())
  127. : [];
  128. $types = [...$currentReturnTypes, ...$docblockReturnTypes];
  129. if (! $types) {
  130. throw DataTransferObjectError::untypedCollection($type);
  131. }
  132. return $this->normaliseTypes(...$types);
  133. }
  134. /**
  135. * @return string[]
  136. */
  137. private function getCurrentReturnTypesFromDocblock(string $definition): array
  138. {
  139. $DOCBLOCK_REGEX = '/@method ((?:(?:[\w?|\\\\<>])+(?:\[])?)+) current/';
  140. preg_match(
  141. $DOCBLOCK_REGEX,
  142. $definition,
  143. $matches
  144. );
  145. $type = $matches[1] ?? null;
  146. if (! $type) {
  147. return [];
  148. }
  149. return explode('|', $type);
  150. }
  151. protected function normaliseTypes(?string ...$types): array
  152. {
  153. return array_filter(array_map(
  154. fn (?string $type) => self::$typeMapping[$type] ?? $type,
  155. $types
  156. ));
  157. }
  158. }