Repository: rectorphp/rector-symfony (php)

Issue:
Convert Commands without configure method to invokable command
Simple commands without configure method should also be converted to invokable commands (without Command base class).

```diff

#[AsCommand('foo', 'Foo description')]
-final class FooCommand extends Command
+final class FooCommand
{
-    protected function execute(InputInterface $input, OutputInterface $output): int
+    public function __invoke(OutputInterface $output): int
    {
        return Command::SUCCESS;
    }
}
```


Pull request description:
[Symfony 7.3] Handle commands without configure method in InvokableCommandInputAttributeRector
closes #796

Patch:
diff --git a/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php b/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php
index ee77655a..2fecd491 100644
--- a/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php
+++ b/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php
@@ -136,13 +136,7 @@ CODE_SAMPLE
             return null;
         }
 
-        // 1. fetch configure method to get arguments and options metadata
-        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
-        if (! $configureClassMethod instanceof ClassMethod) {
-            return null;
-        }
-
-        // 2. rename execute to __invoke
+        // 1. rename execute to __invoke
         $executeClassMethod = $node->getMethod(CommandMethodName::EXECUTE);
         if (! $executeClassMethod instanceof ClassMethod) {
             return null;
@@ -151,19 +145,27 @@ CODE_SAMPLE
         $executeClassMethod->name = new Identifier('__invoke');
         $this->visibilityManipulator->makePublic($executeClassMethod);
 
-        // 3. create arguments and options parameters
-        // @todo
-        $commandArguments = $this->commandArgumentsAndOptionsResolver->collectCommandArguments(
-            $configureClassMethod
-        );
+        // 2. fetch configure method to get arguments and options metadata
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
 
-        $commandOptions = $this->commandArgumentsAndOptionsResolver->collectCommandOptions($configureClassMethod);
+        if ($configureClassMethod instanceof ClassMethod) {
+            // 3. create arguments and options parameters
+            // @todo
+            $commandArguments = $this->commandArgumentsAndOptionsResolver->collectCommandArguments(
+                $configureClassMethod
+            );
 
-        // 4. remove configure() method
-        $this->removeConfigureClassMethod($node);
+            $commandOptions = $this->commandArgumentsAndOptionsResolver->collectCommandOptions($configureClassMethod);
+
+            // 4. remove configure() method
+            $this->removeConfigureClassMethod($node);
+
+            // 5. decorate __invoke method with attributes
+            $invokeParams = $this->commandInvokeParamsFactory->createParams($commandArguments, $commandOptions);
+        } else {
+            $invokeParams = [];
+        }
 
-        // 5. decorate __invoke method with attributes
-        $invokeParams = $this->commandInvokeParamsFactory->createParams($commandArguments, $commandOptions);
         $executeClassMethod->params = array_merge($invokeParams, [$executeClassMethod->params[1]]);
 
         // 6. remove parent class
@@ -181,8 +183,10 @@ CODE_SAMPLE
             }
         }
 
-        // 7. replace input->getArgument() and input->getOption() calls with direct variable access
-        $this->replaceInputArgumentOptionFetchWithVariables($executeClassMethod);
+        if ($configureClassMethod instanceof ClassMethod) {
+            // 7. replace input->getArgument() and input->getOption() calls with direct variable access
+            $this->replaceInputArgumentOptionFetchWithVariables($executeClassMethod);
+        }
 
         return $node;
     }


Test patch:
diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/without_configure_method.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/without_configure_method.php.inc
new file mode 100644
index 00000000..bab16684
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/without_configure_method.php.inc
@@ -0,0 +1,43 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(name: 'some_name')]
+final class WithoutConfigure extends Command
+{
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(name: 'some_name')]
+final class WithoutConfigure
+{
+    public function __invoke(OutputInterface $output): int
+    {
+        // ...
+
+        return 1;
+    }
+}
+
+?>