diff --git a/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php b/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php index a8e2867a..74d802d9 100644 --- a/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php +++ b/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php @@ -7,10 +7,10 @@ class GetFromInlineValidator extends GetFromInlineValidatorBase { - protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool + protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool { // Only use this validator for body params if there's no "// Query parameters" comment above - $comments = $validationAssignmentExpression->getComments(); + $comments = $validationStatement->getComments(); $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments)); if (strpos(strtolower($comments), "query parameters") !== false) { return false; diff --git a/src/Extracting/Strategies/GetFromInlineValidatorBase.php b/src/Extracting/Strategies/GetFromInlineValidatorBase.php index bd13c83e..d279b2b1 100644 --- a/src/Extracting/Strategies/GetFromInlineValidatorBase.php +++ b/src/Extracting/Strategies/GetFromInlineValidatorBase.php @@ -5,6 +5,10 @@ use Knuckles\Camel\Extraction\ExtractedEndpointData; use Knuckles\Scribe\Extracting\MethodAstParser; use Knuckles\Scribe\Extracting\ParsesValidationRules; +use Knuckles\Scribe\Extracting\ValidationRulesFinders\RequestValidate; +use Knuckles\Scribe\Extracting\ValidationRulesFinders\ThisValidate; +use Knuckles\Scribe\Extracting\ValidationRulesFinders\ValidatorMake; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; @@ -27,44 +31,26 @@ public function __invoke(ExtractedEndpointData $endpointData, array $routeRules) public function lookForInlineValidationRules(ClassMethod $methodAst): array { - // Validation usually happens early on, so let's assume it's in the first 6 statements - $statements = array_slice($methodAst->stmts, 0, 6); - - $validationRules = null; - $validationAssignmentExpression = null; - $index = null; - foreach ($statements as $index => $node) { - // Filter to only assignment expressions - if (!($node instanceof Node\Stmt\Expression) || !($node->expr instanceof Node\Expr\Assign)) { - continue; - } - - $validationAssignmentExpression = $node->expr; - $rvalue = $validationAssignmentExpression->expr; - - // Look for $validated = $request->validate(...) - if ( - $rvalue instanceof Node\Expr\MethodCall && $rvalue->var instanceof Node\Expr\Variable - && in_array($rvalue->var->name, ["request", "req"]) && $rvalue->name->name == "validate" - ) { - $validationRules = $rvalue->args[0]->value; - break; - } else if ( - // Try $validator = Validator::make(...) - $rvalue instanceof Node\Expr\StaticCall && !empty($rvalue->class->parts) && end($rvalue->class->parts) == "Validator" - && $rvalue->name->name == "make" - ) { - $validationRules = $rvalue->args[1]->value; - break; - } + // Validation usually happens early on, so let's assume it's in the first 10 statements + $statements = array_slice($methodAst->stmts, 0, 10); + + // Todo remove in future + if (method_exists($this, 'isAssignmentMeantForThisStrategy')) { + c::error("A custom strategy of yours is using a removed method isAssignmentMeantForThisStrategy().\n"); + c::error("Fix this by changing the method name to isValidationStatementMeantForThisStrategy()\n"); + c::error("and changing the type of its argument to Node.\n"); + exit(1); } - if ($validationAssignmentExpression && !$this->isAssignmentMeantForThisStrategy($validationAssignmentExpression)) { + [$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements); + + if ($validationStatement && + !$this->isValidationStatementMeantForThisStrategy($validationStatement)) { return [[], []]; } // If validation rules were saved in a variable (like $rules), - // find the var and expand the value + // 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 ( @@ -144,8 +130,27 @@ protected function shouldCastUserExample() return true; } - protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool + 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]; + } } diff --git a/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php b/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php index e5cfe149..15408e93 100644 --- a/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php +++ b/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php @@ -7,10 +7,10 @@ class GetFromInlineValidator extends GetFromInlineValidatorBase { - protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool + protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool { // Only use this validator for query params if there's a "// Query parameters" comment above - $comments = $validationAssignmentExpression->getComments(); + $comments = $validationStatement->getComments(); $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments)); if (strpos(strtolower($comments), "query parameters") !== false) { return true; diff --git a/src/Extracting/ValidationRulesFinders/RequestValidate.php b/src/Extracting/ValidationRulesFinders/RequestValidate.php new file mode 100644 index 00000000..8ca7fe36 --- /dev/null +++ b/src/Extracting/ValidationRulesFinders/RequestValidate.php @@ -0,0 +1,41 @@ +validate(...); + * or just + * $request->validate(...); + * + * Also supports `$req` instead of `$request` + * Also supports `->validateWithBag('', ...)` + */ +class RequestValidate +{ + public static function find(Node $node) + { + if (!($node instanceof Node\Stmt\Expression)) return; + + $expr = $node->expr; + if ($expr instanceof Node\Expr\Assign) { + $expr = $expr->expr; // If it's an assignment, get the expression on the RHS + } + + if ( + $expr instanceof Node\Expr\MethodCall + && $expr->var instanceof Node\Expr\Variable + && in_array($expr->var->name, ["request", "req"]) + ) { + if ($expr->name->name == "validate") { + return $expr->args[0]->value; + } + + if ($expr->name->name == "validateWithBag") { + return $expr->args[1]->value; + } + } + } +} \ No newline at end of file diff --git a/src/Extracting/ValidationRulesFinders/ThisValidate.php b/src/Extracting/ValidationRulesFinders/ThisValidate.php new file mode 100644 index 00000000..d5f5092e --- /dev/null +++ b/src/Extracting/ValidationRulesFinders/ThisValidate.php @@ -0,0 +1,36 @@ +validate($request, ...); + * or just + * $this->validate($request, ...); + * + * Also supports `$req` instead of `$request` + */ +class ThisValidate +{ + public static function find(Node $node) + { + if (!($node instanceof Node\Stmt\Expression)) return; + + $expr = $node->expr; + if ($expr instanceof Node\Expr\Assign) { + $expr = $expr->expr; // If it's an assignment, get the expression on the RHS + } + + if ( + $expr instanceof Node\Expr\MethodCall + && $expr->var instanceof Node\Expr\Variable + && $expr->var->name === "this" + ) { + if ($expr->name->name == "validate") { + return $expr->args[1]->value; + } + } + } +} \ No newline at end of file diff --git a/src/Extracting/ValidationRulesFinders/ValidatorMake.php b/src/Extracting/ValidationRulesFinders/ValidatorMake.php new file mode 100644 index 00000000..ca3a774e --- /dev/null +++ b/src/Extracting/ValidationRulesFinders/ValidatorMake.php @@ -0,0 +1,34 @@ +expr instanceof Node\Expr\Assign)) { + return; + } + + $expr = $node->expr->expr; // Get the expression on the RHS + + if ( + $expr instanceof Node\Expr\StaticCall + && !empty($expr->class->parts) + && end($expr->class->parts) == "Validator" + && $expr->name->name == "make" + ) { + return $expr->args[1]->value; + } + } +} \ No newline at end of file diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 5e0f38b4..7d942e54 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -446,6 +446,31 @@ public function withInlineRequestValidate(Request $request) // Do stuff } + public function withInlineRequestValidateNoAssignment(Request $request) + { + $request->validate([ + // The id of the user. Example: 9 + 'user_id' => 'int|required', + // The id of the room. + 'room_id' => ['string', 'in:3,5,6'], + // Whether to ban the user forever. Example: false + 'forever' => 'boolean', + // Just need something here + 'another_one' => 'numeric', + 'even_more_param' => 'array', + 'book.name' => 'string', + 'book.author_id' => 'integer', + 'book.pages_count' => 'integer', + 'ids.*' => 'integer', + // The first name of the user. Example: John + 'users.*.first_name' => ['string'], + // The last name of the user. Example: Doe + 'users.*.last_name' => 'string', + ]); + + // Do stuff + } + public function withInlineRequestValidateQueryParams(Request $request) { // Query parameters @@ -501,6 +526,57 @@ public function withInlineValidatorMake(Request $request) } } + public function withInlineRequestValidateWithBag(Request $request) + { + $request->validateWithBag('stuff', [ + // The id of the user. Example: 9 + 'user_id' => 'int|required', + // The id of the room. + 'room_id' => ['string', 'in:3,5,6'], + // Whether to ban the user forever. Example: false + 'forever' => 'boolean', + // Just need something here + 'another_one' => 'numeric', + 'even_more_param' => 'array', + 'book.name' => 'string', + 'book.author_id' => 'integer', + 'book.pages_count' => 'integer', + 'ids.*' => 'integer', + // The first name of the user. Example: John + 'users.*.first_name' => ['string'], + // The last name of the user. Example: Doe + 'users.*.last_name' => 'string', + ]); + + // Do stuff + } + + + public function withInlineThisValidate(Request $request) + { + $this->validate($request, [ + // The id of the user. Example: 9 + 'user_id' => 'int|required', + // The id of the room. + 'room_id' => ['string', 'in:3,5,6'], + // Whether to ban the user forever. Example: false + 'forever' => 'boolean', + // Just need something here + 'another_one' => 'numeric', + 'even_more_param' => 'array', + 'book.name' => 'string', + 'book.author_id' => 'integer', + 'book.pages_count' => 'integer', + 'ids.*' => 'integer', + // The first name of the user. Example: John + 'users.*.first_name' => ['string'], + // The last name of the user. Example: Doe + 'users.*.last_name' => 'string', + ]); + + // Do stuff + } + public function withInjectedModel(TestUser $user) { return null; diff --git a/tests/Strategies/GetFromInlineValidatorTest.php b/tests/Strategies/GetFromInlineValidatorTest.php index e2bce79c..add6ce15 100644 --- a/tests/Strategies/GetFromInlineValidatorTest.php +++ b/tests/Strategies/GetFromInlineValidatorTest.php @@ -89,7 +89,7 @@ class GetFromInlineValidatorTest extends BaseLaravelTest ]; /** @test */ - public function can_fetch_from_request_validate() + public function can_fetch_from_request_validate_assignment() { $endpoint = new class extends ExtractedEndpointData { public function __construct(array $parameters = []) @@ -105,6 +105,57 @@ public function __construct(array $parameters = []) $this->assertIsArray($results['ids']['example']); } + /** @test */ + public function can_fetch_from_request_validate_expression() + { + $endpoint = new class extends ExtractedEndpointData { + public function __construct(array $parameters = []) + { + $this->method = new \ReflectionMethod(TestController::class, 'withInlineRequestValidateNoAssignment'); + } + }; + + $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([])); + $results = $strategy($endpoint, []); + + $this->assertArraySubset(self::$expected, $results); + $this->assertIsArray($results['ids']['example']); + } + + /** @test */ + public function can_fetch_from_request_validatewithbag() + { + $endpoint = new class extends ExtractedEndpointData { + public function __construct(array $parameters = []) + { + $this->method = new \ReflectionMethod(TestController::class, 'withInlineRequestValidateWithBag'); + } + }; + + $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([])); + $results = $strategy($endpoint, []); + + $this->assertArraySubset(self::$expected, $results); + $this->assertIsArray($results['ids']['example']); + } + + /** @test */ + public function can_fetch_from_this_validate() + { + $endpoint = new class extends ExtractedEndpointData { + public function __construct(array $parameters = []) + { + $this->method = new \ReflectionMethod(TestController::class, 'withInlineThisValidate'); + } + }; + + $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([])); + $results = $strategy($endpoint, []); + + $this->assertArraySubset(self::$expected, $results); + $this->assertIsArray($results['ids']['example']); + } + /** @test */ public function can_fetch_from_validator_make() {