From fcc9ce41d25f4c418eb64df84ee1f1d7c7a578b5 Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Sat, 4 Oct 2025 22:12:35 +0200 Subject: [PATCH 1/5] Refactor attempts to get request data to use type-specific methods when the type is known --- docs/rector_rules_overview.md | 21 +- .../RequestInputToTypedMethodRector.php | 236 ++++++++++++++++++ .../Fixture/array_access_with_cast.php.inc | 37 +++ .../Fixture/cast_to_boolean.php.inc | 33 +++ .../Fixture/cast_to_float.php.inc | 33 +++ .../Fixture/cast_to_integer.php.inc | 35 +++ .../Fixture/cast_to_string.php.inc | 35 +++ .../Fixture/property_fetch_with_cast.php.inc | 37 +++ .../Fixture/skip_already_typed.php.inc | 24 ++ .../RequestInputToTypedMethodRectorTest.php | 31 +++ .../configured_rule_without_configuration.php | 10 + 11 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 src/Rector/MethodCall/RequestInputToTypedMethodRector.php create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/array_access_with_cast.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_boolean.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_float.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_integer.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_string.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/property_fetch_with_cast.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/RequestInputToTypedMethodRectorTest.php create mode 100644 tests/Rector/MethodCall/RequestInputToTypedMethodRector/config/configured_rule_without_configuration.php diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 9c9c8501..d7a3afec 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 86 Rules Overview +# 87 Rules Overview ## AbortIfRector @@ -1378,6 +1378,25 @@ Change if report to report_if
+## RequestInputToTypedMethodRector + +Refactor Request input/get/data methods and array access to type-specific methods when the type is known + +- class: [`RectorLaravel\Rector\MethodCall\RequestInputToTypedMethodRector`](../src/Rector/MethodCall/RequestInputToTypedMethodRector.php) + +```diff +-$name = $request->input('name'); +-$age = (int) $request->get('age'); +-$price = (float) $request->data('price'); +-$isActive = (bool) $request['is_active']; ++$name = $request->string('name'); ++$age = $request->integer('age'); ++$price = $request->float('price'); ++$isActive = $request->boolean('is_active'); +``` + +
+ ## RequestStaticValidateToInjectRector Change static `validate()` method to `$request->validate()` diff --git a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php new file mode 100644 index 00000000..4ba88f03 --- /dev/null +++ b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php @@ -0,0 +1,236 @@ +input('name'); +$age = (int) $request->get('age'); +$price = (float) $request->data('price'); +$isActive = (bool) $request['is_active']; +CODE_SAMPLE, + <<<'CODE_SAMPLE' +$name = $request->string('name'); +$age = $request->integer('age'); +$price = $request->float('price'); +$isActive = $request->boolean('is_active'); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Cast::class, Assign::class]; + } + + /** + * @param Cast|Assign $node + */ + public function refactor(Node $node): ?Node + { + if ($node instanceof Cast) { + return $this->refactorCast($node); + } + + if ($node instanceof Assign) { + return $this->refactorAssign($node); + } + + return null; + } + + private function refactorCast(Cast $cast): ?Node + { + $expr = $cast->expr; + + if ($expr instanceof MethodCall) { + $typedMethod = $this->getTypedMethodFromCast($cast); + if ($typedMethod !== null && $this->isRequestMethodCall($expr)) { + return $this->replaceWithTypedMethod($expr, $typedMethod); + } + } + + if ($expr instanceof ArrayDimFetch) { + $typedMethod = $this->getTypedMethodFromCast($cast); + if ($typedMethod !== null && $this->isRequestArrayAccess($expr)) { + return $this->convertArrayAccessToTypedMethod($expr, $typedMethod); + } + } + + if ($expr instanceof PropertyFetch) { + $typedMethod = $this->getTypedMethodFromCast($cast); + if ($typedMethod !== null && $this->isRequestPropertyFetch($expr)) { + return $this->convertPropertyFetchToTypedMethod($expr, $typedMethod); + } + } + + return null; + } + + private function refactorAssign(Assign $assign): ?Node + { + $expr = $assign->expr; + + if ($expr instanceof MethodCall && $this->isRequestMethodCall($expr)) { + $typedMethod = $this->inferTypeFromContext($assign); + if ($typedMethod !== null) { + $assign->expr = $this->replaceWithTypedMethod($expr, $typedMethod); + return $assign; + } + } + + if ($expr instanceof ArrayDimFetch && $this->isRequestArrayAccess($expr)) { + $typedMethod = $this->inferTypeFromContext($assign); + if ($typedMethod !== null) { + $assign->expr = $this->convertArrayAccessToTypedMethod($expr, $typedMethod); + return $assign; + } + } + + if ($expr instanceof PropertyFetch && $this->isRequestPropertyFetch($expr)) { + $typedMethod = $this->inferTypeFromContext($assign); + if ($typedMethod !== null) { + $assign->expr = $this->convertPropertyFetchToTypedMethod($expr, $typedMethod); + return $assign; + } + } + + return null; + } + + private function isRequestMethodCall(MethodCall $methodCall): bool + { + if (! $this->isObjectType($methodCall->var, new ObjectType('Illuminate\Http\Request'))) { + return false; + } + + $methodName = $this->getName($methodCall->name); + return $methodName !== null && in_array($methodName, self::GENERIC_METHODS, true); + } + + private function isRequestArrayAccess(ArrayDimFetch $arrayDimFetch): bool + { + return $arrayDimFetch->var instanceof Variable + && $this->isObjectType($arrayDimFetch->var, new ObjectType('Illuminate\Http\Request')); + } + + private function isRequestPropertyFetch(PropertyFetch $propertyFetch): bool + { + return $propertyFetch->var instanceof Variable + && $this->isObjectType($propertyFetch->var, new ObjectType('Illuminate\Http\Request')); + } + + private function getTypedMethodFromCast(Cast $cast): ?string + { + return match (true) { + $cast instanceof String_ => 'string', + $cast instanceof Int_ => 'integer', + $cast instanceof Double => 'float', + $cast instanceof Bool_ => 'boolean', + default => null, + }; + } + + private function inferTypeFromContext(Assign $assign): ?string + { + if (! $assign->var instanceof Variable) { + return null; + } + + $varType = $this->nodeTypeResolver->getType($assign->var); + + if ($varType->isString()->yes()) { + return 'string'; + } + + if ($varType->isInteger()->yes()) { + return 'integer'; + } + + if ($varType->isFloat()->yes()) { + return 'float'; + } + + if ($varType->isBoolean()->yes()) { + return 'boolean'; + } + + $objectClassNames = $varType->getObjectClassNames(); + if (in_array('Carbon\Carbon', $objectClassNames, true) || in_array('Illuminate\Support\Carbon', $objectClassNames, true)) { + return 'date'; + } + + return null; + } + + private function replaceWithTypedMethod(MethodCall $methodCall, string $typedMethod): MethodCall + { + $methodCall->name = new Identifier($typedMethod); + return $methodCall; + } + + private function convertArrayAccessToTypedMethod(ArrayDimFetch $arrayDimFetch, string $typedMethod): MethodCall + { + if (! $arrayDimFetch->var instanceof Variable) { + return new MethodCall($arrayDimFetch->var, $typedMethod); + } + + $args = []; + if ($arrayDimFetch->dim !== null) { + $args[] = new Node\Arg($arrayDimFetch->dim); + } + + return new MethodCall($arrayDimFetch->var, $typedMethod, $args); + } + + private function convertPropertyFetchToTypedMethod(PropertyFetch $propertyFetch, string $typedMethod): MethodCall + { + $propertyName = $this->getName($propertyFetch->name); + if ($propertyName === null) { + return new MethodCall($propertyFetch->var, $typedMethod); + } + + return new MethodCall( + $propertyFetch->var, + $typedMethod, + [new Node\Arg(new ScalarString($propertyName))] + ); + } +} diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/array_access_with_cast.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/array_access_with_cast.php.inc new file mode 100644 index 00000000..898b01b4 --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/array_access_with_cast.php.inc @@ -0,0 +1,37 @@ + +----- +string('name'); + $age = $request->integer('age'); + $price = $request->float('price'); + $isActive = $request->boolean('is_active'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_boolean.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_boolean.php.inc new file mode 100644 index 00000000..e949e1f0 --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_boolean.php.inc @@ -0,0 +1,33 @@ +input('is_active'); + $enabled = (bool) $request->get('enabled'); + } +} + +?> +----- +boolean('is_active'); + $enabled = $request->boolean('enabled'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_float.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_float.php.inc new file mode 100644 index 00000000..097a764d --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_float.php.inc @@ -0,0 +1,33 @@ +input('price'); + $amount = (double) $request->get('amount'); + } +} + +?> +----- +float('price'); + $amount = $request->float('amount'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_integer.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_integer.php.inc new file mode 100644 index 00000000..cda9a6bf --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_integer.php.inc @@ -0,0 +1,35 @@ +input('age'); + $count = (int) $request->get('count'); + $quantity = (int) $request->data('quantity'); + } +} + +?> +----- +integer('age'); + $count = $request->integer('count'); + $quantity = $request->integer('quantity'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_string.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_string.php.inc new file mode 100644 index 00000000..17fbcf4e --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/cast_to_string.php.inc @@ -0,0 +1,35 @@ +input('name'); + $email = (string) $request->get('email'); + $address = (string) $request->data('address'); + } +} + +?> +----- +string('name'); + $email = $request->string('email'); + $address = $request->string('address'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/property_fetch_with_cast.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/property_fetch_with_cast.php.inc new file mode 100644 index 00000000..10da566a --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/property_fetch_with_cast.php.inc @@ -0,0 +1,37 @@ +name; + $age = (int) $request->age; + $price = (float) $request->price; + $isActive = (bool) $request->is_active; + } +} + +?> +----- +string('name'); + $age = $request->integer('age'); + $price = $request->float('price'); + $isActive = $request->boolean('is_active'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc new file mode 100644 index 00000000..df1a1a99 --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc @@ -0,0 +1,24 @@ +string('name'); + $age = $request->integer('age'); + $price = $request->float('price'); + $isActive = $request->boolean('is_active'); + $date = $request->date('date'); + + // Should not change if no cast + $data = $request->input('data'); + $value = $request->get('value'); + } +} + +?> diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/RequestInputToTypedMethodRectorTest.php b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/RequestInputToTypedMethodRectorTest.php new file mode 100644 index 00000000..fe23a685 --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/RequestInputToTypedMethodRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule_without_configuration.php'; + } +} diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/config/configured_rule_without_configuration.php b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/config/configured_rule_without_configuration.php new file mode 100644 index 00000000..2f37a436 --- /dev/null +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/config/configured_rule_without_configuration.php @@ -0,0 +1,10 @@ +rule(RequestInputToTypedMethodRector::class); +}; From 1a232799c92ba5e2ad6f52b2c76093eae128b12f Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Sat, 4 Oct 2025 22:12:52 +0200 Subject: [PATCH 2/5] Remove line --- .../Fixture/skip_already_typed.php.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc index df1a1a99..103bbde0 100644 --- a/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc +++ b/tests/Rector/MethodCall/RequestInputToTypedMethodRector/Fixture/skip_already_typed.php.inc @@ -14,7 +14,7 @@ class SkipAlreadyTyped $price = $request->float('price'); $isActive = $request->boolean('is_active'); $date = $request->date('date'); - + // Should not change if no cast $data = $request->input('data'); $value = $request->get('value'); From 76e9b371d888e28af69a0b56e17e32d822726f93 Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 6 Oct 2025 11:55:01 +0200 Subject: [PATCH 3/5] Fix linting --- .../MethodCall/RequestInputToTypedMethodRector.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php index 4ba88f03..354dd3c5 100644 --- a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php +++ b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php @@ -5,6 +5,7 @@ namespace RectorLaravel\Rector\MethodCall; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Cast; @@ -61,7 +62,7 @@ public function getNodeTypes(): array } /** - * @param Cast|Assign $node + * @param Cast|Assign $node */ public function refactor(Node $node): ?Node { @@ -112,6 +113,7 @@ private function refactorAssign(Assign $assign): ?Node $typedMethod = $this->inferTypeFromContext($assign); if ($typedMethod !== null) { $assign->expr = $this->replaceWithTypedMethod($expr, $typedMethod); + return $assign; } } @@ -120,6 +122,7 @@ private function refactorAssign(Assign $assign): ?Node $typedMethod = $this->inferTypeFromContext($assign); if ($typedMethod !== null) { $assign->expr = $this->convertArrayAccessToTypedMethod($expr, $typedMethod); + return $assign; } } @@ -128,6 +131,7 @@ private function refactorAssign(Assign $assign): ?Node $typedMethod = $this->inferTypeFromContext($assign); if ($typedMethod !== null) { $assign->expr = $this->convertPropertyFetchToTypedMethod($expr, $typedMethod); + return $assign; } } @@ -142,6 +146,7 @@ private function isRequestMethodCall(MethodCall $methodCall): bool } $methodName = $this->getName($methodCall->name); + return $methodName !== null && in_array($methodName, self::GENERIC_METHODS, true); } @@ -203,6 +208,7 @@ private function inferTypeFromContext(Assign $assign): ?string private function replaceWithTypedMethod(MethodCall $methodCall, string $typedMethod): MethodCall { $methodCall->name = new Identifier($typedMethod); + return $methodCall; } @@ -214,7 +220,7 @@ private function convertArrayAccessToTypedMethod(ArrayDimFetch $arrayDimFetch, s $args = []; if ($arrayDimFetch->dim !== null) { - $args[] = new Node\Arg($arrayDimFetch->dim); + $args[] = new Arg($arrayDimFetch->dim); } return new MethodCall($arrayDimFetch->var, $typedMethod, $args); @@ -230,7 +236,7 @@ private function convertPropertyFetchToTypedMethod(PropertyFetch $propertyFetch, return new MethodCall( $propertyFetch->var, $typedMethod, - [new Node\Arg(new ScalarString($propertyName))] + [new Arg(new ScalarString($propertyName))] ); } } From 06d7ef332abe6244489e14cf23ba9033c97eb68a Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 6 Oct 2025 11:56:30 +0200 Subject: [PATCH 4/5] Run Rector --- src/Rector/MethodCall/RequestInputToTypedMethodRector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php index 354dd3c5..f472d6da 100644 --- a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php +++ b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php @@ -4,6 +4,7 @@ namespace RectorLaravel\Rector\MethodCall; +use PhpParser\Node\Expr; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\ArrayDimFetch; @@ -219,7 +220,7 @@ private function convertArrayAccessToTypedMethod(ArrayDimFetch $arrayDimFetch, s } $args = []; - if ($arrayDimFetch->dim !== null) { + if ($arrayDimFetch->dim instanceof Expr) { $args[] = new Arg($arrayDimFetch->dim); } From aae5f0cf21167f8270c14015e63c6984a36752bd Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 6 Oct 2025 12:03:23 +0200 Subject: [PATCH 5/5] Code style fixes --- src/Rector/MethodCall/RequestInputToTypedMethodRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php index f472d6da..3246226d 100644 --- a/src/Rector/MethodCall/RequestInputToTypedMethodRector.php +++ b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php @@ -4,9 +4,9 @@ namespace RectorLaravel\Rector\MethodCall; -use PhpParser\Node\Expr; use PhpParser\Node; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Cast;