Skip to content

Commit a91c959

Browse files
authoredJul 2, 2022
Merge pull request #1982 from Haehnchen/feature/907-link-data-class-form
#907 linemarker for linking a data_class to related forms
2 parents bb9a86c + 8479264 commit a91c959

File tree

7 files changed

+274
-2
lines changed

7 files changed

+274
-2
lines changed
 

‎src/main/java/fr/adrienbrault/idea/symfony2plugin/form/PhpLineMarkerProvider.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
import com.intellij.codeInsight.daemon.LineMarkerProvider;
55
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
66
import com.intellij.psi.PsiElement;
7+
import com.intellij.psi.search.GlobalSearchScope;
8+
import com.intellij.util.indexing.FileBasedIndex;
79
import com.jetbrains.php.lang.psi.elements.PhpClass;
810
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
911
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
12+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
1013
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
14+
import org.apache.commons.lang.StringUtils;
1115
import org.jetbrains.annotations.NotNull;
1216
import org.jetbrains.annotations.Nullable;
1317

1418
import java.util.Collection;
19+
import java.util.HashSet;
1520
import java.util.List;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
1623

1724
/**
1825
* @author Daniel Espendiller <daniel@espendiller.net>
@@ -33,13 +40,48 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
3340
for (PsiElement psiElement : psiElements) {
3441
if (PhpElementsUtil.getClassNamePattern().accepts(psiElement)) {
3542
attachFormDataClass(lineMarkerInfos, psiElement);
43+
attachPhpClassToFormDataClass(lineMarkerInfos, psiElement);
3644
}
3745
}
3846
}
3947

48+
private void attachPhpClassToFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
49+
PsiElement phpClassContext = leaf.getContext();
50+
if(!(phpClassContext instanceof PhpClass)) {
51+
return;
52+
}
53+
54+
String fqn = ((PhpClass) phpClassContext).getFQN();
55+
56+
Set<String> classes = FileBasedIndex.getInstance().getValues(FormDataClassStubIndex.KEY, "\\" + StringUtils.stripStart(fqn, "\\"), GlobalSearchScope.allScope(leaf.getProject()))
57+
.stream()
58+
.flatMap(Set::stream)
59+
.collect(Collectors.toSet());
60+
61+
Collection<PhpClass> phpClasses = new HashSet<>();
62+
for (String clazz: classes) {
63+
phpClasses.addAll(PhpElementsUtil.getClassesInterface(leaf.getProject(), clazz));
64+
}
65+
66+
if (!phpClasses.isEmpty()) {
67+
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.FORM_TYPE_LINE_MARKER)
68+
.setTargets(phpClasses)
69+
.setTooltipText("Navigate to form");
70+
71+
lineMarkerInfos.add(builder.createLineMarkerInfo(leaf));
72+
}
73+
}
74+
4075
private void attachFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
4176
PsiElement phpClassContext = leaf.getContext();
42-
if(!(phpClassContext instanceof PhpClass) || !PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface")) {
77+
if (phpClassContext == null) {
78+
return;
79+
}
80+
81+
boolean isFormTypeInstance = PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface")
82+
|| PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormExtensionInterface");
83+
84+
if (!isFormTypeInstance) {
4385
return;
4486
}
4587

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
2+
3+
import com.intellij.psi.PsiElement;
4+
import com.intellij.psi.PsiFile;
5+
import com.intellij.psi.PsiRecursiveElementVisitor;
6+
import com.intellij.psi.util.PsiTreeUtil;
7+
import com.intellij.util.indexing.*;
8+
import com.intellij.util.io.DataExternalizer;
9+
import com.intellij.util.io.EnumeratorStringDescriptor;
10+
import com.intellij.util.io.KeyDescriptor;
11+
import com.jetbrains.php.lang.PhpFileType;
12+
import com.jetbrains.php.lang.psi.PhpFile;
13+
import com.jetbrains.php.lang.psi.elements.*;
14+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.StringSetDataExternalizer;
15+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
16+
import gnu.trove.THashMap;
17+
import org.apache.commons.lang.StringUtils;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import java.util.HashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
/**
26+
* public function configureOptions(OptionsResolver $resolver)
27+
* {
28+
* $resolver->setDefaults(['data_class' => XXX]);
29+
* $resolver->setDefault('data_class', XXX);
30+
* }
31+
*
32+
* @author Daniel Espendiller <daniel@espendiller.net>
33+
*/
34+
public class FormDataClassStubIndex extends FileBasedIndexExtension<String, Set<String>> {
35+
public static final ID<String, Set<String>> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.form_data_class");
36+
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
37+
38+
@NotNull
39+
@Override
40+
public ID<String, Set<String>> getName() {
41+
return KEY;
42+
}
43+
44+
@NotNull
45+
@Override
46+
public DataIndexer<String, Set<String>, FileContent> getIndexer() {
47+
return inputData -> {
48+
Map<String, Set<String>> map = new THashMap<>();
49+
50+
PsiFile psiFile = inputData.getPsiFile();
51+
if(!(psiFile instanceof PhpFile)) {
52+
return map;
53+
}
54+
55+
psiFile.accept(new PsiRecursiveElementVisitor() {
56+
@Override
57+
public void visitElement(@NotNull PsiElement element) {
58+
if (element instanceof MethodReference) {
59+
String phpClassFqn = null;
60+
61+
String name = ((MethodReference) element).getName();
62+
if ("setDefault".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
63+
// $resolver->setDefault('data_class', XXX);
64+
65+
ParameterList parameterList = ((MethodReference) element).getParameterList();
66+
if (parameterList != null) {
67+
PsiElement parameter = parameterList.getParameter(0);
68+
if (parameter instanceof StringLiteralExpression) {
69+
String contents = ((StringLiteralExpression) parameter).getContents();
70+
if ("data_class".equals(contents)) {
71+
PsiElement parameter1 = parameterList.getParameter(1);
72+
if (parameter1 != null) {
73+
phpClassFqn = getString(parameter1);
74+
}
75+
}
76+
}
77+
}
78+
} else if ("setDefaults".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
79+
// $resolver->setDefaults(['data_class' => XXX]);
80+
81+
ParameterList parameterList = ((MethodReference) element).getParameterList();
82+
if (parameterList != null) {
83+
PsiElement parameter = parameterList.getParameter(0);
84+
if (parameter instanceof ArrayCreationExpression) {
85+
PhpPsiElement dataClassPsiElement = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameter, "data_class");
86+
if (dataClassPsiElement != null) {
87+
phpClassFqn = getString(dataClassPsiElement);
88+
}
89+
}
90+
}
91+
}
92+
93+
if (phpClassFqn != null) {
94+
Method methodScope = PsiTreeUtil.getParentOfType(element, Method.class);
95+
if (methodScope != null) {
96+
PhpClass parentOfType = methodScope.getContainingClass();
97+
if (parentOfType != null) {
98+
map.putIfAbsent(phpClassFqn, new HashSet<>());
99+
map.get(phpClassFqn).add(parentOfType.getFQN());
100+
}
101+
}
102+
}
103+
}
104+
105+
super.visitElement(element);
106+
}
107+
108+
@Nullable
109+
private String getString(@NotNull PsiElement parameter) {
110+
if (parameter instanceof ClassConstantReference) {
111+
String classConstantPhpFqn = PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) parameter);
112+
if (StringUtils.isNotBlank(classConstantPhpFqn)) {
113+
return "\\" + StringUtils.stripStart(classConstantPhpFqn, "\\");
114+
}
115+
} else if (parameter instanceof StringLiteralExpression) {
116+
String contents1 = ((StringLiteralExpression) parameter).getContents();
117+
if (StringUtils.isNotBlank(contents1)) {
118+
return "\\" + StringUtils.stripStart(contents1, "\\");
119+
}
120+
}
121+
122+
return null;
123+
}
124+
});
125+
126+
return map;
127+
};
128+
129+
}
130+
131+
@NotNull
132+
@Override
133+
public KeyDescriptor<String> getKeyDescriptor() {
134+
return this.myKeyDescriptor;
135+
}
136+
137+
@NotNull
138+
@Override
139+
public DataExternalizer<Set<String>> getValueExternalizer() {
140+
return new StringSetDataExternalizer();
141+
}
142+
143+
@NotNull
144+
@Override
145+
public FileBasedIndex.InputFilter getInputFilter() {
146+
return file -> file.getFileType() == PhpFileType.INSTANCE;
147+
}
148+
149+
@Override
150+
public boolean dependsOnFileContent() {
151+
return true;
152+
}
153+
154+
@Override
155+
public int getVersion() {
156+
return 1;
157+
}
158+
}

‎src/main/resources/META-INF/plugin.xml

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerIdUsagesStubIndex"/>
228228
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigBlockIndexExtension"/>
229229
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigControllerStubIndex"/>
230+
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex"/>
230231

231232
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.ServiceLineMarkerProvider"/>
232233
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ControllerMethodLineMarkerProvider"/>

‎src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/PhpLineMarkerProviderTest.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected String getTestDataPath() {
1717
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/fixtures";
1818
}
1919

20-
public void testThatRouteLineMarkerForControllerIsGiven() {
20+
public void testThatFormCanNavigateToDataClass() {
2121
assertLineMarker(
2222
myFixture.configureByText(
2323
PhpFileType.INSTANCE,
@@ -38,6 +38,17 @@ public void testThatRouteLineMarkerForControllerIsGiven() {
3838
),
3939
new LineMarker.ToolTipEqualsAssert("Navigate to data class")
4040
);
41+
}
4142

43+
public void testThatDataClassCanNavigateToForm() {
44+
assertLineMarker(
45+
myFixture.configureByText(
46+
PhpFileType.INSTANCE,
47+
"<?php\n" +
48+
"namespace App;\n" +
49+
"class FoobarDataClass {}\n"
50+
),
51+
new LineMarker.ToolTipEqualsAssert("Navigate to form")
52+
);
4253
}
4354
}

‎src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/fixtures/classes.php

+10
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ public function getExtendedType() {
7575
}
7676

7777
}
78+
79+
class FormTypeDataClass
80+
{
81+
public function configureOptions(OptionsResolver $resolver)
82+
{
83+
$resolver->setDefaults(array(
84+
'data_class' => \App\FoobarDataClass::class,
85+
));
86+
}
87+
}
7888
}
7989

8090
namespace Form\FormType {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.stubs.indexes;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
/**
7+
* @author Daniel Espendiller <daniel@espendiller.net>
8+
*
9+
* @see fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex
10+
*/
11+
public class FormDataClassStubIndexTest extends SymfonyLightCodeInsightFixtureTestCase {
12+
13+
public void setUp() throws Exception {
14+
super.setUp();
15+
16+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("FormDataClassStubIndex.php"));
17+
}
18+
19+
public String getTestDataPath() {
20+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/stubs/indexes/fixtures";
21+
}
22+
23+
public void testTemplateIncludeIndexer() {
24+
assertIndexContains(FormDataClassStubIndex.KEY, "\\App\\FooDataClass1", "\\App\\FooDataClass2", "\\App\\FooDataClass3");
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App {
4+
5+
use Symfony\Component\OptionsResolver\OptionsResolver;
6+
7+
class FooDataClass1 {}
8+
class FooDataClass2 {}
9+
class FooDataClass3 {}
10+
11+
class AutoFarmType
12+
{
13+
public function configureOptions(OptionsResolver $resolver)
14+
{
15+
$resolver->setDefaults([
16+
'data_class' => FooDataClass1::class,
17+
]);
18+
19+
$resolver->setDefault('data_class', FooDataClass2::class);
20+
21+
$resolver->setDefault('data_class', 'App\FooDataClass3');
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)
Please sign in to comment.