Skip to content

Commit 84ec688

Browse files
authored
Merge pull request #1701 from Haehnchen/feature/php8-attribute-doctrine
Add PHP8 attributes support for Doctrine metadata
2 parents 9b09e22 + 69884b0 commit 84ec688

File tree

8 files changed

+382
-1
lines changed

8 files changed

+382
-1
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/DoctrineUtil.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
import com.jetbrains.php.lang.psi.PhpFile;
1515
import com.jetbrains.php.lang.psi.elements.PhpClass;
1616
import com.jetbrains.php.lang.psi.elements.PhpPsiElement;
17+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionArgument;
18+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionClassConstantArgument;
1719
import de.espend.idea.php.annotation.util.AnnotationUtil;
1820
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.visitor.AnnotationElementWalkingVisitor;
21+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.visitor.AttributeElementWalkingVisitor;
22+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
1923
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
2024
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
2125
import org.apache.commons.lang.ArrayUtils;
@@ -113,6 +117,8 @@ private static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull
113117
public static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull PsiElement phpFile) {
114118
final Collection<Pair<String, String>> pairs = new ArrayList<>();
115119

120+
// Annotations:
121+
// @ORM\Entity("repositoryClass": YYY)
116122
phpFile.acceptChildren(new AnnotationElementWalkingVisitor(phpDocTag -> {
117123
PhpDocComment phpDocComment = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
118124
if (phpDocComment == null) {
@@ -134,6 +140,27 @@ public static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull P
134140
return false;
135141
}, MODEL_CLASS_ANNOTATION));
136142

143+
// Attributes:
144+
// #[Entity(repositoryClass: UserRepository::class)]
145+
phpFile.acceptChildren(new AttributeElementWalkingVisitor(pair -> {
146+
String repositoryClass = null;
147+
148+
PhpExpectedFunctionArgument argument = PhpElementsUtil.findAttributeArgumentByName("repositoryClass", pair.getFirst());
149+
if (argument instanceof PhpExpectedFunctionClassConstantArgument) {
150+
String repositoryClassRaw = ((PhpExpectedFunctionClassConstantArgument) argument).getClassFqn();
151+
if (StringUtils.isNotBlank(repositoryClassRaw)) {
152+
repositoryClass = repositoryClassRaw;
153+
}
154+
}
155+
156+
pairs.add(Pair.create(
157+
StringUtils.stripStart(pair.getSecond().getFQN(), "\\"),
158+
repositoryClass != null ? StringUtils.stripStart(repositoryClass, "\\") : null
159+
));
160+
161+
return false;
162+
}, MODEL_CLASS_ANNOTATION));
163+
137164
return pairs;
138165
}
139166

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver;
2+
3+
import com.intellij.psi.PsiFile;
4+
import com.jetbrains.php.lang.psi.PhpFile;
5+
import com.jetbrains.php.lang.psi.elements.Field;
6+
import com.jetbrains.php.lang.psi.elements.PhpAttribute;
7+
import com.jetbrains.php.lang.psi.elements.PhpClass;
8+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionArgument;
9+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionClassConstantArgument;
10+
import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelField;
11+
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.dict.DoctrineMetadataModel;
12+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
13+
import org.apache.commons.lang.StringUtils;
14+
import org.jetbrains.annotations.NotNull;
15+
16+
import java.util.ArrayList;
17+
import java.util.Collection;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
/**
22+
*
23+
* example:
24+
* - "#[Column(type: "decimal", precision: 2, scale: 1)]"
25+
*
26+
* @link https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/reference/attributes-reference.html#attrref_table
27+
* @author Daniel Espendiller <daniel@espendiller.net>
28+
*/
29+
public class DoctrinePhpAttributeMappingDriver implements DoctrineMappingDriverInterface {
30+
@Override
31+
public DoctrineMetadataModel getMetadata(@NotNull DoctrineMappingDriverArguments arguments) {
32+
PsiFile psiFile = arguments.getPsiFile();
33+
if(!(psiFile instanceof PhpFile)) {
34+
return null;
35+
}
36+
37+
Collection<DoctrineModelField> fields = new ArrayList<>();
38+
DoctrineMetadataModel model = new DoctrineMetadataModel(fields);
39+
40+
for (PhpClass phpClass : PhpElementsUtil.getClassesInterface(arguments.getProject(), arguments.getClassName())) {
41+
for (PhpAttribute attribute : phpClass.getAttributes()) {
42+
String fqn = attribute.getFQN();
43+
if (fqn == null) {
44+
continue;
45+
}
46+
47+
if (!PhpElementsUtil.isEqualClassName(fqn, "\\Doctrine\\ORM\\Mapping\\Table")) {
48+
continue;
49+
}
50+
51+
String name = PhpElementsUtil.findAttributeArgumentByNameAsString("name", attribute);
52+
if (name != null) {
53+
model.setTable(name);
54+
}
55+
}
56+
57+
Map<String, Map<String, String>> maps = new HashMap<>();
58+
for(Field field: phpClass.getFields()) {
59+
if (field.isConstant()) {
60+
continue;
61+
}
62+
63+
DoctrineModelField doctrineModelField = new DoctrineModelField(field.getName());
64+
doctrineModelField.addTarget(field);
65+
66+
boolean isField = false;
67+
for (PhpAttribute attribute : field.getAttributes()) {
68+
String fqn = attribute.getFQN();
69+
if (fqn == null) {
70+
continue;
71+
}
72+
73+
if (PhpElementsUtil.isEqualClassName(fqn, "\\Doctrine\\ORM\\Mapping\\Column")) {
74+
isField = true;
75+
76+
String name = PhpElementsUtil.findAttributeArgumentByNameAsString("name", attribute);
77+
if (name != null) {
78+
doctrineModelField.setColumn(name);
79+
}
80+
81+
String type = PhpElementsUtil.findAttributeArgumentByNameAsString("type", attribute);
82+
if (type != null) {
83+
doctrineModelField.setTypeName(type);
84+
}
85+
}
86+
87+
if (PhpElementsUtil.isEqualClassName(fqn, "\\Doctrine\\ORM\\Mapping\\OneToOne", "\\Doctrine\\ORM\\Mapping\\ManyToOne", "\\Doctrine\\ORM\\Mapping\\OneToMany", "\\Doctrine\\ORM\\Mapping\\ManyToMany")) {
88+
isField = true;
89+
90+
String substring = fqn.substring(fqn.lastIndexOf("\\") + 1);
91+
doctrineModelField.setRelationType(substring);
92+
93+
PhpExpectedFunctionArgument argument = PhpElementsUtil.findAttributeArgumentByName("targetEntity", attribute);
94+
if (argument instanceof PhpExpectedFunctionClassConstantArgument) {
95+
String repositoryClassRaw = ((PhpExpectedFunctionClassConstantArgument) argument).getClassFqn();
96+
if (StringUtils.isNotBlank(repositoryClassRaw)) {
97+
doctrineModelField.setRelation(repositoryClassRaw);
98+
}
99+
}
100+
}
101+
}
102+
103+
if (isField) {
104+
fields.add(doctrineModelField);
105+
}
106+
}
107+
}
108+
109+
return model;
110+
}
111+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/util/DoctrineMetadataUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ public class DoctrineMetadataUtil {
4040
private static final Key<CachedValue<Set<String>>> CLASS_KEYS = new Key<>("CLASS_KEYS");
4141

4242
private static DoctrineMappingDriverInterface[] MAPPING_DRIVERS = new DoctrineMappingDriverInterface[] {
43+
new DoctrinePhpMappingDriver(),
44+
new DoctrinePhpAttributeMappingDriver(),
4345
new DoctrineXmlMappingDriver(),
4446
new DoctrineYamlMappingDriver(),
45-
new DoctrinePhpMappingDriver(),
4647
};
4748

4849
@NotNull
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes.visitor;
2+
3+
import com.intellij.openapi.util.Pair;
4+
import com.intellij.psi.PsiElement;
5+
import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
6+
import com.intellij.util.Processor;
7+
import com.jetbrains.php.lang.psi.elements.PhpAttribute;
8+
import com.jetbrains.php.lang.psi.elements.PhpAttributesList;
9+
import com.jetbrains.php.lang.psi.elements.PhpClass;
10+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
/**
14+
* Visit class attributes; filtered by instance
15+
*
16+
* @author Daniel Espendiller <daniel@espendiller.net>
17+
*/
18+
public class AttributeElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor {
19+
20+
@NotNull
21+
private final Processor<Pair<PhpAttribute, PhpClass>> phpDocTagProcessor;
22+
23+
@NotNull
24+
private final String[] annotations;
25+
26+
public AttributeElementWalkingVisitor(@NotNull Processor<Pair<PhpAttribute, PhpClass>> phpDocTagProcessor, @NotNull String... annotations) {
27+
this.phpDocTagProcessor = phpDocTagProcessor;
28+
this.annotations = annotations;
29+
}
30+
31+
@Override
32+
public void visitElement(@NotNull PsiElement element) {
33+
if ((element instanceof PhpAttributesList)) {
34+
visitPhpAttributesList((PhpAttributesList) element);
35+
}
36+
37+
super.visitElement(element);
38+
}
39+
40+
private void visitPhpAttributesList(@NotNull PhpAttributesList phpAttributesList) {
41+
PsiElement parent = phpAttributesList.getParent();
42+
43+
if (parent instanceof PhpClass) {
44+
for (PhpAttribute attribute : phpAttributesList.getAttributes()) {
45+
String fqn = attribute.getFQN();
46+
if (fqn == null) {
47+
continue;
48+
}
49+
50+
if (PhpElementsUtil.isEqualClassName(fqn, annotations)) {
51+
this.phpDocTagProcessor.process(Pair.create(attribute, (PhpClass) parent));
52+
}
53+
}
54+
}
55+
}
56+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import com.jetbrains.php.lang.psi.elements.impl.ConstantImpl;
2727
import com.jetbrains.php.lang.psi.elements.impl.PhpDefineImpl;
2828
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
29+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionArgument;
30+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionScalarArgument;
2931
import com.jetbrains.php.phpunit.PhpUnitUtil;
3032
import com.jetbrains.php.refactoring.PhpAliasImporter;
3133
import fr.adrienbrault.idea.symfony2plugin.dic.MethodReferenceBag;
@@ -785,6 +787,16 @@ public static boolean isEqualClassName(@Nullable PhpClass phpClass, @Nullable St
785787
.equals(StringUtils.stripStart(compareClassName, "\\"));
786788
}
787789

790+
public static boolean isEqualClassName(@NotNull String phpClass, @NotNull String ...compareClassNames) {
791+
for (String compareClassName : compareClassNames) {
792+
if (Objects.equals(StringUtils.stripStart(phpClass, "\\"), StringUtils.stripStart(compareClassName, "\\"))) {
793+
return true;
794+
}
795+
}
796+
797+
return false;
798+
}
799+
788800
@NotNull
789801
public static PsiElement[] getMethodParameterReferences(@NotNull Method method, int parameterIndex) {
790802
// we dont have a parameter on resolved method
@@ -1502,6 +1514,31 @@ public static String getMethodReferenceStringValueParameter(@NotNull MethodRefer
15021514
return null;
15031515
}
15041516

1517+
public static PhpExpectedFunctionArgument findAttributeArgumentByName(@NotNull String attributeName, @NotNull PhpAttribute phpAttribute) {
1518+
for (PhpAttribute.PhpAttributeArgument argument : phpAttribute.getArguments()) {
1519+
String name = argument.getName();
1520+
if (!attributeName.equals(name)) {
1521+
continue;
1522+
}
1523+
1524+
return argument.getArgument();
1525+
}
1526+
1527+
return null;
1528+
}
1529+
1530+
@Nullable
1531+
public static String findAttributeArgumentByNameAsString(@NotNull String attributeName, @NotNull PhpAttribute phpAttribute) {
1532+
PhpExpectedFunctionArgument attributeArgumentByName = findAttributeArgumentByName(attributeName, phpAttribute);
1533+
if (attributeArgumentByName instanceof PhpExpectedFunctionScalarArgument) {
1534+
String value = PsiElementUtils.trimQuote(attributeArgumentByName.getValue());
1535+
if (StringUtils.isNotBlank(value)) {
1536+
return value;
1537+
}
1538+
}
1539+
1540+
return null;
1541+
}
15051542
/**
15061543
* Visit and collect all variables in given scope
15071544
*/

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/DoctrineUtilTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,43 @@ public void testGetClassRepositoryPairForStringValue() {
3939
assertEquals("MyBundle\\Entity\\Repository\\AddressRepository", next.getSecond());
4040
}
4141

42+
/**
43+
* @see DoctrineUtil#getClassRepositoryPair
44+
*/
45+
public void testGetClassRepositoryPairForPhp8AttributeStringValue() {
46+
myFixture.configureByText(PhpFileType.INSTANCE, "<?php class Foobar {};");
47+
48+
PsiFile psiFileFromText = PhpPsiElementFactory.createPsiFileFromText(getProject(), "" +
49+
"<?php\n" +
50+
"namespace Foo;\n" +
51+
"\n" +
52+
"use Foobar;\n" +
53+
"use Doctrine\\ORM\\Mapping\\Entity;\n" +
54+
"\n" +
55+
"#[Entity(repositoryClass: Foobar::class, readOnly: false)]\n" +
56+
"class Apple\n" +
57+
"{\n" +
58+
"}\n" +
59+
"\n" +
60+
"\n" +
61+
"#[Entity()]\n" +
62+
"class Car\n" +
63+
"{\n" +
64+
"}\n" +
65+
"\n"
66+
);
67+
68+
Collection<Pair<String, String>> classRepositoryPair = DoctrineUtil.getClassRepositoryPair(psiFileFromText);
69+
70+
Pair<String, String> apple = classRepositoryPair.stream().filter(stringStringPair -> "Foo\\Apple".equals(stringStringPair.getFirst())).findFirst().get();
71+
assertEquals("Foo\\Apple", apple.getFirst());
72+
assertEquals("Foobar", apple.getSecond());
73+
74+
Pair<String, String> car = classRepositoryPair.stream().filter(stringStringPair -> "Foo\\Car".equals(stringStringPair.getFirst())).findFirst().get();
75+
assertEquals("Foo\\Car", car.getFirst());
76+
assertNull(car.getSecond());
77+
}
78+
4279
/**
4380
* @see DoctrineUtil#getClassRepositoryPair
4481
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.doctrine.metadata.driver;
2+
3+
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
4+
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.dict.DoctrineMetadataModel;
5+
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver.DoctrineMappingDriverArguments;
6+
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver.DoctrinePhpAttributeMappingDriver;
7+
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver.DoctrinePhpMappingDriver;
8+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
9+
10+
/**
11+
* @author Daniel Espendiller <daniel@espendiller.net>
12+
*
13+
* @see fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver.DoctrinePhpAttributeMappingDriver
14+
*/
15+
public class DoctrinePhpAttributeMappingDriverTest extends SymfonyLightCodeInsightFixtureTestCase {
16+
public void setUp() throws Exception {
17+
super.setUp();
18+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("attributes.php"));
19+
}
20+
21+
public String getTestDataPath() {
22+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/driver/fixtures";
23+
}
24+
25+
/**
26+
* @see DoctrinePhpMappingDriver#getMetadata(fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.driver.DoctrineMappingDriverArguments)
27+
*/
28+
public void testPhpAttributesMetadata() {
29+
DoctrineMetadataModel metadata = createOrmMetadata();
30+
31+
assertEquals("table_name", metadata.getTable());
32+
33+
assertEquals("string", metadata.getField("email").getTypeName());
34+
assertEquals("string", metadata.getField("emailTrait").getTypeName());
35+
36+
assertEquals("\\ORM\\Foobar\\Egg", metadata.getField("apple").getRelation());
37+
assertEquals("ManyToOne", metadata.getField("apple").getRelationType());
38+
39+
assertEquals("\\ORM\\Foobar\\Egg", metadata.getField("egg").getRelation());
40+
assertEquals("ManyToMany", metadata.getField("egg").getRelationType());
41+
42+
assertEquals("\\ORM\\Foobar\\Egg", metadata.getField("address").getRelation());
43+
assertEquals("OneToOne", metadata.getField("address").getRelationType());
44+
45+
assertEquals("\\ORM\\Foobar\\Egg", metadata.getField("phonenumbers").getRelation());
46+
assertEquals("OneToMany", metadata.getField("phonenumbers").getRelationType());
47+
48+
assertEquals("\\Doctrine\\Orm\\MyTrait\\Egg", metadata.getField("appleTrait").getRelation());
49+
assertEquals("ManyToOne", metadata.getField("appleTrait").getRelationType());
50+
}
51+
52+
private DoctrineMetadataModel createOrmMetadata() {
53+
return new DoctrinePhpAttributeMappingDriver().getMetadata(
54+
new DoctrineMappingDriverArguments(getProject(), PhpPsiElementFactory.createPsiFileFromText(getProject(), "<?php $foo = null;"), "\\ORM\\Attributes\\AttributeEntity")
55+
);
56+
}
57+
}

0 commit comments

Comments
 (0)