123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- <?php
- namespace Knuckles\Scribe\Extracting\Strategies;
- use Knuckles\Camel\Extraction\ExtractedEndpointData;
- use Knuckles\Scribe\Extracting\MethodAstParser;
- use Knuckles\Scribe\Extracting\ParsesValidationRules;
- use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidate;
- use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ThisValidate;
- use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ValidatorMake;
- use PhpParser\Node;
- use PhpParser\Node\Stmt\ClassMethod;
- class GetFromInlineValidatorBase extends Strategy
- {
- use ParsesValidationRules;
- public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
- {
- if (!$endpointData->method instanceof \ReflectionMethod) {
- return [];
- }
- $methodAst = MethodAstParser::getMethodAst($endpointData->method);
- [$validationRules, $customParameterData] = $this->lookForInlineValidationRules($methodAst);
- $bodyParametersFromValidationRules = $this->getParametersFromValidationRules($validationRules, $customParameterData);
- return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
- }
- public function lookForInlineValidationRules(ClassMethod $methodAst): array
- {
- // Validation usually happens early on, so let's assume it's in the first 10 statements
- $statements = array_slice($methodAst->stmts, 0, 10);
- [$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements);
- if ($validationStatement &&
- !$this->isValidationStatementMeantForThisStrategy($validationStatement)) {
- return [[], []];
- }
- // If validation rules were saved in a variable (like $rules),
- // try to find the var and expand the value
- if ($validationRules instanceof Node\Expr\Variable) {
- foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
- if (
- $earlierStatement instanceof Node\Stmt\Expression
- && $earlierStatement->expr instanceof Node\Expr\Assign
- && $earlierStatement->expr->var instanceof Node\Expr\Variable
- && $earlierStatement->expr->var->name == $validationRules->name
- ) {
- $validationRules = $earlierStatement->expr->expr;
- break;
- }
- }
- }
- if (!$validationRules instanceof Node\Expr\Array_) {
- return [[], []];
- }
- $rules = [];
- $customParameterData = [];
- foreach ($validationRules->items as $item) {
- /** @var Node\Expr\ArrayItem $item */
- if (!$item->key instanceof Node\Scalar\String_) {
- continue;
- }
- $paramName = $item->key->value;
- // Might be an expression or concatenated string, etc.
- // For now, let's focus on simple strings and arrays of strings
- if ($item->value instanceof Node\Scalar\String_) {
- $rules[$paramName] = $item->value->value;
- } else if ($item->value instanceof Node\Expr\Array_) {
- $rulesList = [];
- foreach ($item->value->items as $arrayItem) {
- /** @var Node\Expr\ArrayItem $arrayItem */
- if ($arrayItem->value instanceof Node\Scalar\String_) {
- $rulesList[] = $arrayItem->value->value;
- }
- // Try to extract Enum rule
- else if (
- function_exists('enum_exists') &&
- ($enum = $this->extractEnumClassFromArrayItem($arrayItem)) &&
- enum_exists($enum) && method_exists($enum, 'tryFrom')
- ) {
- // $case->value only exists on BackedEnums, not UnitEnums
- // method_exists($enum, 'tryFrom') implies $enum instanceof BackedEnum
- // @phpstan-ignore-next-line
- $rulesList[] = 'in:' . implode(',', array_map(fn ($case) => $case->value, $enum::cases()));
- }
- }
- $rules[$paramName] = join('|', $rulesList);
- } else {
- $rules[$paramName] = [];
- continue;
- }
- $dataFromComment = [];
- $comments = join("\n", array_map(
- fn($comment) => ltrim(ltrim($comment->getReformattedText(), "/")),
- $item->getComments()
- ));
- if ($comments) {
- if (str_contains($comments, 'No-example')) $dataFromComment['example'] = null;
- $dataFromComment['description'] = trim(str_replace(['No-example.', 'No-example'], '', $comments));
- if (preg_match('/(.*\s+|^)Example:\s*([\s\S]+)\s*/s', $dataFromComment['description'], $matches)) {
- $dataFromComment['description'] = trim($matches[1]);
- $dataFromComment['example'] = $matches[2];
- }
- }
- $customParameterData[$paramName] = $dataFromComment;
- }
- return [$rules, $customParameterData];
- }
- protected function extractEnumClassFromArrayItem(Node\Expr\ArrayItem $arrayItem): ?string
- {
- $args = [];
- // Enum rule with the form "new Enum(...)"
- if ($arrayItem->value instanceof Node\Expr\New_ &&
- $arrayItem->value->class instanceof Node\Name &&
- last($arrayItem->value->class->parts) === 'Enum'
- ) {
- $args = $arrayItem->value->args;
- }
- // Enum rule with the form "Rule::enum(...)"
- else if ($arrayItem->value instanceof Node\Expr\StaticCall &&
- $arrayItem->value->class instanceof Node\Name &&
- last($arrayItem->value->class->parts) === 'Rule' &&
- $arrayItem->value->name instanceof Node\Identifier &&
- $arrayItem->value->name->name === 'enum'
- ) {
- $args = $arrayItem->value->args;
- }
- if (count($args) !== 1 || !$args[0] instanceof Node\Arg) return null;
- $arg = $args[0];
- if ($arg->value instanceof Node\Expr\ClassConstFetch &&
- $arg->value->class instanceof Node\Name
- ) {
- return '\\' . implode('\\', $arg->value->class->parts);
- } else if ($arg->value instanceof Node\Scalar\String_) {
- return $arg->value->value;
- }
- return null;
- }
- protected function getMissingCustomDataMessage($parameterName)
- {
- return "No extra data found for parameter '$parameterName' from your inline validator. You can add a comment above '$parameterName' with a description and example.";
- }
- protected function shouldCastUserExample()
- {
- return true;
- }
- protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
- {
- return true;
- }
- protected function findValidationExpression($statements): ?array
- {
- $strategies = [
- RequestValidate::class, // $request->validate(...);
- ValidatorMake::class, // Validator::make($request, ...)
- ThisValidate::class, // $this->validate(...);
- ];
- foreach ($statements as $index => $node) {
- foreach ($strategies as $strategy) {
- if ($validationRules = $strategy::find($node)) {
- return [$index, $node, $validationRules];
- }
- }
- }
- return [null, null, null];
- }
- }
|