diff --git a/config/sets/symfony/symfony-code-quality.php b/config/sets/symfony/symfony-code-quality.php index c4fa32f7e..326a9dcd7 100644 --- a/config/sets/symfony/symfony-code-quality.php +++ b/config/sets/symfony/symfony-code-quality.php @@ -6,6 +6,7 @@ use Rector\Symfony\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector; use Rector\Symfony\CodeQuality\Rector\BinaryOp\RequestIsMainRector; use Rector\Symfony\CodeQuality\Rector\BinaryOp\ResponseStatusCodeRector; +use Rector\Symfony\CodeQuality\Rector\Class_\ControllerMethodInjectionToConstructorRector; use Rector\Symfony\CodeQuality\Rector\Class_\EventListenerToEventSubscriberRector; use Rector\Symfony\CodeQuality\Rector\Class_\InlineClassRoutePrefixRector; use Rector\Symfony\CodeQuality\Rector\Class_\LoadValidatorMetadataToAnnotationRector; @@ -32,7 +33,10 @@ RemoveUnusedRequestParamRector::class, ParamTypeFromRouteRequiredRegexRector::class, + + // controller ActionSuffixRemoverRector::class, + ControllerMethodInjectionToConstructorRector::class, LoadValidatorMetadataToAnnotationRector::class, // request method diff --git a/config/sets/symfony/symfony7/symfony74/symfony74-json-streamer.php b/config/sets/symfony/symfony7/symfony74/symfony74-json-streamer.php index f66db07c1..5bcc25507 100644 --- a/config/sets/symfony/symfony7/symfony74/symfony74-json-streamer.php +++ b/config/sets/symfony/symfony7/symfony74/symfony74-json-streamer.php @@ -9,11 +9,35 @@ return static function (RectorConfig $rectorConfig): void { // @see https://github.com/symfony/symfony/blob/7.4/UPGRADE-7.4.md#jsonstreamer $rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [ - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'getNativeToStreamValueTransformer', 'getValueTransformers'), - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'getStreamToNativeValueTransformers', 'getValueTransformers'), - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'withNativeToStreamValueTransformers', 'withValueTransformers'), - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'withStreamToNativeValueTransformers', 'withValueTransformers'), - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'withAdditionalNativeToStreamValueTransformer', 'withAdditionalValueTransformer'), - new MethodCallRename('Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', 'withAdditionalStreamToNativeValueTransformer', 'withAdditionalValueTransformer'), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'getNativeToStreamValueTransformer', + 'getValueTransformers' + ), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'getStreamToNativeValueTransformers', + 'getValueTransformers' + ), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'withNativeToStreamValueTransformers', + 'withValueTransformers' + ), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'withStreamToNativeValueTransformers', + 'withValueTransformers' + ), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'withAdditionalNativeToStreamValueTransformer', + 'withAdditionalValueTransformer' + ), + new MethodCallRename( + 'Symfony\Component\JsonStreamer\Mapping\PropertyMetadata', + 'withAdditionalStreamToNativeValueTransformer', + 'withAdditionalValueTransformer' + ), ]); }; diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/ControllerMethodInjectionToConstructorRectorTest.php b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/ControllerMethodInjectionToConstructorRectorTest.php new file mode 100644 index 000000000..ed22e6ddf --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/ControllerMethodInjectionToConstructorRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/re_use_existing_service.php.inc b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/re_use_existing_service.php.inc new file mode 100644 index 000000000..78ff55ab1 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/re_use_existing_service.php.inc @@ -0,0 +1,52 @@ +log('level', 'value'); + } +} + +?> +----- +logger->log('level', 'value'); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/skip_scalar_and_request_params.php.inc b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/skip_scalar_and_request_params.php.inc new file mode 100644 index 000000000..49aa37fcc --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/skip_scalar_and_request_params.php.inc @@ -0,0 +1,18 @@ +log('level', 'value'); + } +} + +?> +----- +logger->log('level', 'value'); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/config/configured_rule.php new file mode 100644 index 000000000..8afdaf782 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ControllerMethodInjectionToConstructorRector::class); +}; diff --git a/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php b/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php new file mode 100644 index 000000000..12c82f0ca --- /dev/null +++ b/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php @@ -0,0 +1,169 @@ +getData(); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; + +final class SomeController extends AbstractController +{ + public function __construct( + private readonly SomeService $someService + ) { + } + + #[Route('/some-path', name: 'some_name')] + public function someAction( + Request $request + ) { + $data = $this->someService->getData(); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->controllerAnalyzer->isController($node)) { + return null; + } + + $propertyMetadatas = []; + + foreach ($node->getMethods() as $classMethod) { + if (! $this->controllerMethodAnalyzer->isAction($classMethod)) { + continue; + } + + foreach ($classMethod->getParams() as $key => $param) { + // skip scalar and empty values, as not services + if ($param->type === null || $param->type instanceof Identifier) { + continue; + } + + // request is allowed + if ($param->type instanceof Name && $this->isName($param->type, SymfonyClass::REQUEST)) { + continue; + } + + // @todo allow parameter converter + unset($classMethod->params[$key]); + + $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); + $propertyMetadatas[] = new PropertyMetadata($this->getName($param->var), $paramType); + } + } + + // nothing to move + if ($propertyMetadatas === []) { + return null; + } + + $paramNamesToReplace = []; + foreach ($propertyMetadatas as $propertyMetadata) { + $paramNamesToReplace[] = $propertyMetadata->getName(); + } + + // 1. update constructor + foreach ($propertyMetadatas as $propertyMetadata) { + $this->classDependencyManipulator->addConstructorDependency($node, $propertyMetadata); + } + + foreach ($node->getMethods() as $classMethod) { + if (! $this->controllerMethodAnalyzer->isAction($classMethod)) { + continue; + } + + // replace param use with property fetch + $this->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use ( + $paramNamesToReplace + ): ?PropertyFetch { + if (! $node instanceof Variable) { + return null; + } + + if (! $this->isNames($node, $paramNamesToReplace)) { + return null; + + } + + $propertyName = $this->getName($node); + return new PropertyFetch(new Variable('this'), $propertyName); + }); + } + + // 2. replace in method bodies + + return $node; + } +} diff --git a/rules/Symfony61/Rector/StaticPropertyFetch/ErrorNamesPropertyToConstantRector.php b/rules/Symfony61/Rector/StaticPropertyFetch/ErrorNamesPropertyToConstantRector.php index cce56779d..d21373d72 100644 --- a/rules/Symfony61/Rector/StaticPropertyFetch/ErrorNamesPropertyToConstantRector.php +++ b/rules/Symfony61/Rector/StaticPropertyFetch/ErrorNamesPropertyToConstantRector.php @@ -111,10 +111,10 @@ public function refactor(Node $node): ?Node } private function refactorStaticPropertyFetch( - StaticPropertyFetch $node, + StaticPropertyFetch $staticPropertyFetch, ClassReflection $classReflection ): ?ClassConstFetch { - if (! $this->isName($node->name, 'errorNames')) { + if (! $this->isName($staticPropertyFetch->name, 'errorNames')) { return null; } diff --git a/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php b/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php index 5b606cb7d..6a7085f24 100644 --- a/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php +++ b/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php @@ -182,10 +182,8 @@ private function getArgumentsFromOptionArray(?Arg $optionArgument, array $additi continue; } - if ($mappedName === 'isSafeCallback') { - if ($item->value instanceof MethodCall && $item->value->isFirstClassCallable()) { - continue; - } + if ($mappedName === 'isSafeCallback' && ($item->value instanceof MethodCall && $item->value->isFirstClassCallable())) { + continue; } $arg = new Arg($item->value); diff --git a/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index 87d86b535..db2d78833 100644 --- a/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -78,4 +78,29 @@ public function stream(string $view, array $parameters = [], StreamedResponse $r protected function isGranted($attributes, $subject = null): bool { } + + public function set(string $id, ?object $service) + { + // TODO: Implement set() method. + } + + public function initialized(string $id): bool + { + // TODO: Implement initialized() method. + } + + public function getParameter(string $name) + { + // TODO: Implement getParameter() method. + } + + public function hasParameter(string $name): bool + { + // TODO: Implement hasParameter() method. + } + + public function setParameter(string $name, \UnitEnum|float|array|bool|int|string|null $value) + { + // TODO: Implement setParameter() method. + } }