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..3246226d --- /dev/null +++ b/src/Rector/MethodCall/RequestInputToTypedMethodRector.php @@ -0,0 +1,243 @@ +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 instanceof Expr) { + $args[] = new 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 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..103bbde0 --- /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); +};