Skip to content

Commit 118d426

Browse files
authored
Merge pull request #2453 from Haehnchen/feature/symfony-invoke-template
Add templates+guesser for "Invokable Commands and Input Attributes"
2 parents 6af9115 + 25368b2 commit 118d426

File tree

5 files changed

+167
-6
lines changed

5 files changed

+167
-6
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewCommandAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public void actionPerformed(@NotNull AnActionEvent event) {
8585
PsiElement commandAttributes = PhpBundleFileFactory.createFile(
8686
project,
8787
directory.getVirtualFile(),
88-
NewFileActionUtil.guessCommandTemplateType(project),
88+
NewFileActionUtil.guessCommandTemplateType(project, strings.get(0)),
8989
finalClassName,
9090
hashMap
9191
);

src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewFileActionUtil.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
import com.intellij.openapi.project.Project;
88
import com.intellij.openapi.util.io.StreamUtil;
99
import com.intellij.psi.PsiDirectory;
10+
import com.jetbrains.php.lang.psi.elements.PhpClass;
1011
import com.jetbrains.php.roots.PhpNamespaceCompositeProvider;
11-
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
12-
import fr.adrienbrault.idea.symfony2plugin.util.StringUtils;
13-
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil;
14-
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil;
12+
import fr.adrienbrault.idea.symfony2plugin.util.*;
1513
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle;
1614
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyCommand;
1715
import fr.adrienbrault.idea.symfony2plugin.util.psi.PhpBundleFileFactory;
@@ -64,7 +62,32 @@ public static boolean isInGivenDirectoryScope(@NotNull AnActionEvent event, @Not
6462
return false;
6563
}
6664

67-
public static String guessCommandTemplateType(@NotNull Project project) {
65+
public static String guessCommandTemplateType(@NotNull Project project, @NotNull String namespace) {
66+
// Check if InvokableCommand is available (Symfony 7.3+)
67+
if (PhpElementsUtil.getClassInterface(project, "\\Symfony\\Component\\Console\\Command\\InvokableCommand") != null) {
68+
String normalizedNamespace = "\\" + org.apache.commons.lang3.StringUtils.strip(namespace, "\\") + "\\";
69+
Collection<PhpClass> commandClasses = PhpIndexUtil.getPhpClassInsideNamespace(project, normalizedNamespace);
70+
71+
boolean hasExecuteMethod = false;
72+
73+
// Iterate over each class in the same namespace
74+
for (PhpClass phpClass : commandClasses) {
75+
if (phpClass.getAttributes("\\Symfony\\Component\\Console\\Attribute\\AsCommand").isEmpty() && !PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Console\\Command\\Command")) {
76+
continue;
77+
}
78+
79+
if (phpClass.findOwnMethodByName("execute") != null) {
80+
hasExecuteMethod = true;
81+
break;
82+
}
83+
}
84+
85+
// if existing commands use execute, use invokable template
86+
if (!hasExecuteMethod) {
87+
return "command_invokable";
88+
}
89+
}
90+
6891
if (PhpElementsUtil.getClassInterface(project, "\\Symfony\\Component\\Console\\Attribute\\AsCommand") != null) {
6992
return "command_attributes";
7093
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace {{ namespace }};
6+
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
11+
#[AsCommand(name: '{{ command_name }}', description: 'Hello PhpStorm')]
12+
class {{ class }}
13+
{
14+
public function __invoke(SymfonyStyle $io): int
15+
{
16+
return Command::SUCCESS;
17+
}
18+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.action;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.action.NewFileActionUtil;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
/**
7+
* @author Daniel Espendiller <daniel@espendiller.net>
8+
*/
9+
public class NewFileActionUtilTest extends SymfonyLightCodeInsightFixtureTestCase {
10+
11+
public void setUp() throws Exception {
12+
super.setUp();
13+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("classes.php"));
14+
}
15+
16+
public String getTestDataPath() {
17+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/action/fixtures";
18+
}
19+
20+
public void testGuessCommandTemplateTypeReturnsInvokableForNewNamespace() {
21+
String result = NewFileActionUtil.guessCommandTemplateType(getProject(), "App\\CommandNothing");
22+
assertEquals("command_invokable", result);
23+
}
24+
25+
public void testGuessCommandTemplateTypeReturnsInvokableWhenExistingCommandUsesInvoke() {
26+
// Create a command with __invoke method (extends Command, not InvokableCommand)
27+
myFixture.addFileToProject(
28+
"src/Command/ExistingCommand.php",
29+
"<?php\n" +
30+
"namespace App\\Command;\n" +
31+
"use Symfony\\Component\\Console\\Attribute\\AsCommand;\n" +
32+
"use Symfony\\Component\\Console\\Style\\SymfonyStyle;\n" +
33+
"\n" +
34+
"#[AsCommand(name: 'app:existing')]\n" +
35+
"class ExistingCommand\n" +
36+
"{\n" +
37+
" public function __invoke(SymfonyStyle $io): int\n" +
38+
" {\n" +
39+
" return Command::SUCCESS;\n" +
40+
" }\n" +
41+
"}\n"
42+
);
43+
44+
String result = NewFileActionUtil.guessCommandTemplateType(getProject(), "App\\Command");
45+
assertEquals("command_invokable", result);
46+
}
47+
48+
public void testGuessCommandTemplateTypeFallsBackWhenExistingCommandUsesExecute() {
49+
// Create a command with execute method (uses execute, not __invoke)
50+
myFixture.configureByText(
51+
"ExistingCommand.php",
52+
"<?php\n" +
53+
"namespace App\\CommandConfigure;\n" +
54+
"use Symfony\\Component\\Console\\Attribute\\AsCommand;\n" +
55+
"use Symfony\\Component\\Console\\Command\\Command;\n" +
56+
"use Symfony\\Component\\Console\\Input\\InputInterface;\n" +
57+
"use Symfony\\Component\\Console\\Output\\OutputInterface;\n" +
58+
"\n" +
59+
"#[AsCommand(name: 'app:existing')]\n" +
60+
"class ExistingCommand extends Command\n" +
61+
"{\n" +
62+
" protected function configure(): void\n" +
63+
" {\n" +
64+
" $this->setDescription('Test');\n" +
65+
" }\n" +
66+
"\n" +
67+
" protected function execute(InputInterface $input, OutputInterface $output): int\n" +
68+
" {\n" +
69+
" return Command::SUCCESS;\n" +
70+
" }\n" +
71+
"}\n"
72+
);
73+
74+
String result = NewFileActionUtil.guessCommandTemplateType(getProject(), "App\\CommandConfigure");
75+
assertEquals("command_attributes", result);
76+
}
77+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Symfony\Component\Console\Command
4+
{
5+
class Command
6+
{
7+
}
8+
9+
class InvokableCommand
10+
{
11+
12+
}
13+
}
14+
15+
namespace Symfony\Component\Console\Input
16+
{
17+
interface InputInterface
18+
{
19+
public function getArgument($name);
20+
public function getOption($name);
21+
}
22+
}
23+
24+
namespace Symfony\Component\Console\Output
25+
{
26+
interface OutputInterface
27+
{
28+
public function writeln($messages);
29+
}
30+
}
31+
32+
namespace Symfony\Component\Console\Attribute
33+
{
34+
#[\Attribute(\Attribute::TARGET_CLASS)]
35+
class AsCommand
36+
{
37+
public function __construct(
38+
public ?string $name = null,
39+
public ?string $description = null,
40+
public array $aliases = []
41+
) {}
42+
}
43+
}

0 commit comments

Comments
 (0)