GetFromInlineValidatorBase.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <?php
  2. namespace Knuckles\Scribe\Extracting\Strategies;
  3. use Knuckles\Camel\Extraction\ExtractedEndpointData;
  4. use Knuckles\Scribe\Extracting\MethodAstParser;
  5. use Knuckles\Scribe\Extracting\ParsesValidationRules;
  6. use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidate;
  7. use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ThisValidate;
  8. use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ValidatorMake;
  9. use PhpParser\Node;
  10. use PhpParser\Node\Stmt\ClassMethod;
  11. class GetFromInlineValidatorBase extends Strategy
  12. {
  13. use ParsesValidationRules;
  14. public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
  15. {
  16. if (!$endpointData->method instanceof \ReflectionMethod) {
  17. return [];
  18. }
  19. $methodAst = MethodAstParser::getMethodAst($endpointData->method);
  20. [$validationRules, $customParameterData] = $this->lookForInlineValidationRules($methodAst);
  21. $bodyParametersFromValidationRules = $this->getParametersFromValidationRules($validationRules, $customParameterData);
  22. return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
  23. }
  24. public function lookForInlineValidationRules(ClassMethod $methodAst): array
  25. {
  26. // Validation usually happens early on, so let's assume it's in the first 10 statements
  27. $statements = array_slice($methodAst->stmts, 0, 10);
  28. [$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements);
  29. if ($validationStatement &&
  30. !$this->isValidationStatementMeantForThisStrategy($validationStatement)) {
  31. return [[], []];
  32. }
  33. // If validation rules were saved in a variable (like $rules),
  34. // try to find the var and expand the value
  35. if ($validationRules instanceof Node\Expr\Variable) {
  36. foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
  37. if (
  38. $earlierStatement instanceof Node\Stmt\Expression
  39. && $earlierStatement->expr instanceof Node\Expr\Assign
  40. && $earlierStatement->expr->var instanceof Node\Expr\Variable
  41. && $earlierStatement->expr->var->name == $validationRules->name
  42. ) {
  43. $validationRules = $earlierStatement->expr->expr;
  44. break;
  45. }
  46. }
  47. }
  48. if (!$validationRules instanceof Node\Expr\Array_) {
  49. return [[], []];
  50. }
  51. $rules = [];
  52. $customParameterData = [];
  53. foreach ($validationRules->items as $item) {
  54. /** @var Node\Expr\ArrayItem $item */
  55. if (!$item->key instanceof Node\Scalar\String_) {
  56. continue;
  57. }
  58. $paramName = $item->key->value;
  59. // Might be an expression or concatenated string, etc.
  60. // For now, let's focus on simple strings and arrays of strings
  61. if ($item->value instanceof Node\Scalar\String_) {
  62. $rules[$paramName] = $item->value->value;
  63. } else if ($item->value instanceof Node\Expr\Array_) {
  64. $rulesList = [];
  65. foreach ($item->value->items as $arrayItem) {
  66. /** @var Node\Expr\ArrayItem $arrayItem */
  67. if ($arrayItem->value instanceof Node\Scalar\String_) {
  68. $rulesList[] = $arrayItem->value->value;
  69. }
  70. // Try to extract Enum rule
  71. else if (
  72. function_exists('enum_exists') &&
  73. ($enum = $this->extractEnumClassFromArrayItem($arrayItem)) &&
  74. enum_exists($enum) && method_exists($enum, 'tryFrom')
  75. ) {
  76. // $case->value only exists on BackedEnums, not UnitEnums
  77. // method_exists($enum, 'tryFrom') implies $enum instanceof BackedEnum
  78. // @phpstan-ignore-next-line
  79. $rulesList[] = 'in:' . implode(',', array_map(fn ($case) => $case->value, $enum::cases()));
  80. }
  81. }
  82. $rules[$paramName] = join('|', $rulesList);
  83. } else {
  84. $rules[$paramName] = [];
  85. continue;
  86. }
  87. $dataFromComment = [];
  88. $comments = join("\n", array_map(
  89. fn($comment) => ltrim(ltrim($comment->getReformattedText(), "/")),
  90. $item->getComments()
  91. ));
  92. if ($comments) {
  93. if (str_contains($comments, 'No-example')) $dataFromComment['example'] = null;
  94. $dataFromComment['description'] = trim(str_replace(['No-example.', 'No-example'], '', $comments));
  95. if (preg_match('/(.*\s+|^)Example:\s*([\s\S]+)\s*/s', $dataFromComment['description'], $matches)) {
  96. $dataFromComment['description'] = trim($matches[1]);
  97. $dataFromComment['example'] = $matches[2];
  98. }
  99. }
  100. $customParameterData[$paramName] = $dataFromComment;
  101. }
  102. return [$rules, $customParameterData];
  103. }
  104. protected function extractEnumClassFromArrayItem(Node\Expr\ArrayItem $arrayItem): ?string
  105. {
  106. $args = [];
  107. // Enum rule with the form "new Enum(...)"
  108. if ($arrayItem->value instanceof Node\Expr\New_ &&
  109. $arrayItem->value->class instanceof Node\Name &&
  110. last($arrayItem->value->class->parts) === 'Enum'
  111. ) {
  112. $args = $arrayItem->value->args;
  113. }
  114. // Enum rule with the form "Rule::enum(...)"
  115. else if ($arrayItem->value instanceof Node\Expr\StaticCall &&
  116. $arrayItem->value->class instanceof Node\Name &&
  117. last($arrayItem->value->class->parts) === 'Rule' &&
  118. $arrayItem->value->name instanceof Node\Identifier &&
  119. $arrayItem->value->name->name === 'enum'
  120. ) {
  121. $args = $arrayItem->value->args;
  122. }
  123. if (count($args) !== 1 || !$args[0] instanceof Node\Arg) return null;
  124. $arg = $args[0];
  125. if ($arg->value instanceof Node\Expr\ClassConstFetch &&
  126. $arg->value->class instanceof Node\Name
  127. ) {
  128. return '\\' . implode('\\', $arg->value->class->parts);
  129. } else if ($arg->value instanceof Node\Scalar\String_) {
  130. return $arg->value->value;
  131. }
  132. return null;
  133. }
  134. protected function getMissingCustomDataMessage($parameterName)
  135. {
  136. return "No extra data found for parameter '$parameterName' from your inline validator. You can add a comment above '$parameterName' with a description and example.";
  137. }
  138. protected function shouldCastUserExample()
  139. {
  140. return true;
  141. }
  142. protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
  143. {
  144. return true;
  145. }
  146. protected function findValidationExpression($statements): ?array
  147. {
  148. $strategies = [
  149. RequestValidate::class, // $request->validate(...);
  150. ValidatorMake::class, // Validator::make($request, ...)
  151. ThisValidate::class, // $this->validate(...);
  152. ];
  153. foreach ($statements as $index => $node) {
  154. foreach ($strategies as $strategy) {
  155. if ($validationRules = $strategy::find($node)) {
  156. return [$index, $node, $validationRules];
  157. }
  158. }
  159. }
  160. return [null, null, null];
  161. }
  162. }