GetFromInlineValidatorBase.php 7.6 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\RequestValidateFacade;
  8. use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ThisValidate;
  9. use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ValidatorMake;
  10. use PhpParser\Node;
  11. use PhpParser\Node\Stmt\ClassMethod;
  12. class GetFromInlineValidatorBase extends Strategy
  13. {
  14. use ParsesValidationRules;
  15. public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
  16. {
  17. if (!$endpointData->method instanceof \ReflectionMethod) {
  18. return [];
  19. }
  20. $methodAst = MethodAstParser::getMethodAst($endpointData->method);
  21. [$validationRules, $customParameterData] = $this->lookForInlineValidationRules($methodAst);
  22. $bodyParametersFromValidationRules = $this->getParametersFromValidationRules($validationRules, $customParameterData);
  23. return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
  24. }
  25. public function lookForInlineValidationRules(ClassMethod $methodAst): array
  26. {
  27. // Validation usually happens early on, so let's assume it's in the first 10 statements
  28. $statements = array_slice($methodAst->stmts, 0, 10);
  29. [$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements);
  30. if ($validationStatement &&
  31. !$this->isValidationStatementMeantForThisStrategy($validationStatement)) {
  32. return [[], []];
  33. }
  34. // If validation rules were saved in a variable (like $rules),
  35. // try to find the var and expand the value
  36. if ($validationRules instanceof Node\Expr\Variable) {
  37. foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
  38. if (
  39. $earlierStatement instanceof Node\Stmt\Expression
  40. && $earlierStatement->expr instanceof Node\Expr\Assign
  41. && $earlierStatement->expr->var instanceof Node\Expr\Variable
  42. && $earlierStatement->expr->var->name == $validationRules->name
  43. ) {
  44. $validationRules = $earlierStatement->expr->expr;
  45. break;
  46. }
  47. }
  48. }
  49. if (!$validationRules instanceof Node\Expr\Array_) {
  50. return [[], []];
  51. }
  52. $rules = [];
  53. $customParameterData = [];
  54. foreach ($validationRules->items as $item) {
  55. /** @var Node\ArrayItem $item */
  56. if (!$item->key instanceof Node\Scalar\String_) {
  57. continue;
  58. }
  59. $paramName = $item->key->value;
  60. // Might be an expression or concatenated string, etc.
  61. // For now, let's focus on simple strings and arrays of strings
  62. if ($item->value instanceof Node\Scalar\String_) {
  63. $rules[$paramName] = $item->value->value;
  64. } else if ($item->value instanceof Node\Expr\Array_) {
  65. $rulesList = [];
  66. foreach ($item->value->items as $arrayItem) {
  67. /** @var Node\ArrayItem $arrayItem */
  68. if ($arrayItem->value instanceof Node\Scalar\String_) {
  69. $rulesList[] = $arrayItem->value->value;
  70. }
  71. // Try to extract Enum rule
  72. else if (
  73. function_exists('enum_exists') &&
  74. ($enum = $this->extractEnumClassFromArrayItem($arrayItem)) &&
  75. enum_exists($enum) && method_exists($enum, 'tryFrom')
  76. ) {
  77. // $case->value only exists on BackedEnums, not UnitEnums
  78. // method_exists($enum, 'tryFrom') implies $enum instanceof BackedEnum
  79. // @phpstan-ignore-next-line
  80. $rulesList[] = 'in:' . implode(',', array_map(fn ($case) => $case->value, $enum::cases()));
  81. }
  82. }
  83. $rules[$paramName] = join('|', $rulesList);
  84. } else {
  85. $rules[$paramName] = [];
  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\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. str_ends_with($arrayItem->value->class->name, '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. str_ends_with($arrayItem->value->class->name, '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 '\\' . $arg->value->class->name;
  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. RequestValidateFacade::class, // Request::validate(...);
  151. ValidatorMake::class, // Validator::make($request, ...)
  152. ThisValidate::class, // $this->validate(...);
  153. ];
  154. foreach ($statements as $index => $node) {
  155. foreach ($strategies as $strategy) {
  156. if ($validationRules = $strategy::find($node)) {
  157. return [$index, $node, $validationRules];
  158. }
  159. }
  160. }
  161. return [null, null, null];
  162. }
  163. }