diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml index 312827c0f..33dfe3ba3 100644 --- a/META-INF/plugin.xml +++ b/META-INF/plugin.xml @@ -76,8 +76,11 @@ + + + diff --git a/src/fr/adrienbrault/idea/symfony2plugin/Symfony2InterfacesUtil.java b/src/fr/adrienbrault/idea/symfony2plugin/Symfony2InterfacesUtil.java index 0945a17e0..3c8b2b73e 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/Symfony2InterfacesUtil.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/Symfony2InterfacesUtil.java @@ -40,6 +40,22 @@ public boolean isUrlGeneratorGenerateCall(PsiElement e) { }); } + public boolean isGetRepositoryCall(PsiElement e) { + return isCallTo(e, new Method[] { + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ManagerRegistry", "getRepository"), + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ObjectManager", "getRepository"), + }); + } + + public boolean isObjectRepositoryCall(PsiElement e) { + return isCallTo(e, new Method[] { + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ObjectRepository", "find"), + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ObjectRepository", "findOneBy"), + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ObjectRepository", "findAll"), + getInterfaceMethod(e.getProject(), "\\Doctrine\\Common\\Persistence\\ObjectRepository", "findBy"), + }); + } + protected boolean isCallTo(PsiElement e, Method expectedMethod) { return isCallTo(e, new Method[] { expectedMethod }); } @@ -156,7 +172,7 @@ protected boolean isImplementationOfInterface(PhpClass phpClass, PhpClass phpInt return isImplementationOfInterface(phpClass.getSuperClass(), phpInterface); } - protected boolean isInstanceOf(PhpClass subjectClass, PhpClass expectedClass) { + public boolean isInstanceOf(PhpClass subjectClass, PhpClass expectedClass) { if (subjectClass == expectedClass) { return true; } diff --git a/src/fr/adrienbrault/idea/symfony2plugin/Symfony2ProjectComponent.java b/src/fr/adrienbrault/idea/symfony2plugin/Symfony2ProjectComponent.java index 4bbdf4709..edbf01480 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/Symfony2ProjectComponent.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/Symfony2ProjectComponent.java @@ -9,6 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile; import fr.adrienbrault.idea.symfony2plugin.dic.ServiceMap; import fr.adrienbrault.idea.symfony2plugin.dic.ServiceMapParser; +import fr.adrienbrault.idea.symfony2plugin.doctrine.component.EntityNamesParser; import fr.adrienbrault.idea.symfony2plugin.routing.Route; import org.jetbrains.annotations.NotNull; import org.xml.sax.SAXException; @@ -36,6 +37,9 @@ public class Symfony2ProjectComponent implements ProjectComponent { private Map routes; private Long routesLastModified; + private Long entityNamespacesMapLastModified; + private Map entityNamespaces; + public Symfony2ProjectComponent(Project project) { this.project = project; } @@ -167,4 +171,32 @@ private String getPath(Project project, String path) { return path; } + public Map getEntityNamespacesMap() { + + String defaultServiceMapFilePath = getPath(project, Settings.getInstance(project).pathToProjectContainer); + + File xmlFile = new File(defaultServiceMapFilePath); + if (!xmlFile.exists()) { + return new HashMap(); + } + + Long xmlFileLastModified = xmlFile.lastModified(); + if (xmlFileLastModified.equals(entityNamespacesMapLastModified)) { + return entityNamespaces; + } + + try { + EntityNamesParser entityNamesParser = new EntityNamesParser(); + entityNamespaces = entityNamesParser.parse(xmlFile); + entityNamespacesMapLastModified = xmlFileLastModified; + + return entityNamespaces; + } catch (SAXException ignored) { + } catch (IOException ignored) { + } catch (ParserConfigurationException ignored) { + } + + return new HashMap(); + } + } diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/DoctrineEntityReferenceContributor.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/DoctrineEntityReferenceContributor.java new file mode 100644 index 000000000..f1c7f958c --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/DoctrineEntityReferenceContributor.java @@ -0,0 +1,45 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine; + +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.*; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + */ +public class DoctrineEntityReferenceContributor extends PsiReferenceContributor { + + @Override + public void registerReferenceProviders(PsiReferenceRegistrar psiReferenceRegistrar) { + psiReferenceRegistrar.registerReferenceProvider( + PlatformPatterns.psiElement(StringLiteralExpression.class), + new PsiReferenceProvider() { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { + if (!(psiElement.getContext() instanceof ParameterList)) { + return new PsiReference[0]; + } + ParameterList parameterList = (ParameterList) psiElement.getContext(); + + if (!(parameterList.getContext() instanceof MethodReference)) { + return new PsiReference[0]; + } + MethodReference method = (MethodReference) parameterList.getContext(); + Symfony2InterfacesUtil interfacesUtil = new Symfony2InterfacesUtil(); + if (!interfacesUtil.isGetRepositoryCall(method)) { + return new PsiReference[0]; + } + + return new PsiReference[]{ new EntityReference((StringLiteralExpression) psiElement) }; + } + } + ); + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java new file mode 100644 index 000000000..a7e25925a --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java @@ -0,0 +1,67 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine; + +import com.intellij.openapi.project.Project; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class EntityHelper { + + /** + * + * @param project PHPStorm projects + * @param shortcutName name as MyBundle\Entity\Model or MyBundle:Model + * @return null|PhpClass + */ + public static PhpClass resolveShortcutName(Project project, String shortcutName) { + + if(shortcutName == null) { + return null; + } + + String entity_name = shortcutName; + + // resolve: + // MyBundle:Model -> MyBundle\Entity\Model + // MyBundle:Folder\Model -> MyBundle\Entity\Folder\Model + if (shortcutName.contains(":")) { + + Symfony2ProjectComponent symfony2ProjectComponent = project.getComponent(Symfony2ProjectComponent.class); + Map em = symfony2ProjectComponent.getEntityNamespacesMap(); + + int firstDirectorySeparatorIndex = shortcutName.indexOf(":"); + + String bundlename = shortcutName.substring(0, firstDirectorySeparatorIndex); + String entityName = shortcutName.substring(firstDirectorySeparatorIndex + 1); + + String namespace = em.get(bundlename); + + if(namespace == null) { + return null; + } + + entity_name = namespace + "\\" + entityName; + } + + // only use them on entity namespace + if(!entity_name.contains("\\")) { + return null; + } + + // dont we have any unique class getting method here? + PhpIndex phpIndex = PhpIndex.getInstance(project); + Collection entity_classes = phpIndex.getClassesByFQN(entity_name); + if(!entity_classes.isEmpty()){ + return entity_classes.iterator().next(); + } + + return null; + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityReference.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityReference.java new file mode 100644 index 000000000..e2d9ea81d --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityReference.java @@ -0,0 +1,109 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine; + +import com.intellij.psi.*; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.*; +import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineEntityLookupElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * @author Daniel Espendiller + */ +public class EntityReference extends PsiReferenceBase implements PsiReference { + private String entityName; + + public EntityReference(@NotNull StringLiteralExpression element) { + super(element); + + entityName = element.getText().substring( + element.getValueRange().getStartOffset(), + element.getValueRange().getEndOffset() + ); + } + + @Nullable + @Override + public PsiElement resolve() { + + PhpClass entity = EntityHelper.resolveShortcutName(getElement().getProject(), this.entityName); + if(entity != null) { + return new PsiElementResolveResult(entity).getElement(); + } + + return null; + } + + @NotNull + @Override + public Object[] getVariants() { + + PhpIndex phpIndex = PhpIndex.getInstance(getElement().getProject()); + + Symfony2ProjectComponent symfony2ProjectComponent = getElement().getProject().getComponent(Symfony2ProjectComponent.class); + Map entityNamespaces = symfony2ProjectComponent.getEntityNamespacesMap(); + + List results = new ArrayList(); + + // find Repository interface to filter RepositoryClasses out + PhpClass repositoryClass = getRepositoryClass(phpIndex); + if(null == repositoryClass) { + return results.toArray(); + } + + for (Map.Entry entry : entityNamespaces.entrySet()) { + + // search for classes that match the symfony2 namings + Collection entities = phpIndex.getNamespacesByName(entry.getValue()); + + // @TODO: it looks like PhpIndex cant search for classes like \ns\Path\*\... + // temporary only use flat entities and dont support "MyBundle:Folder\Entity" + for (PhpNamespace entity_files : entities) { + + // build our symfony2 shortcut + String filename = entity_files.getContainingFile().getName(); + String className = filename.substring(0, filename.lastIndexOf('.')); + String repoName = entry.getKey() + ':' + className; + + // dont add Repository classes and abstract entities + PhpClass entityClass = getClass(phpIndex, entityNamespaces.get(entry.getKey()) + "\\" + className); + if(null != entityClass && isEntity(entityClass, repositoryClass)) { + results.add(new DoctrineEntityLookupElement(repoName, entityClass)); + } + + } + + } + + return results.toArray(); + } + + @Nullable + protected PhpClass getRepositoryClass(PhpIndex phpIndex) { + Collection classes = phpIndex.getInterfacesByFQN("\\Doctrine\\Common\\Persistence\\ObjectRepository"); + return classes.isEmpty() ? null : classes.iterator().next(); + } + + @Nullable + protected PhpClass getClass(PhpIndex phpIndex, String className) { + Collection classes = phpIndex.getClassesByFQN(className); + return classes.isEmpty() ? null : classes.iterator().next(); + } + + protected boolean isEntity(PhpClass entityClass, PhpClass repositoryClass) { + + if(entityClass.isAbstract()) { + return false; + } + + Symfony2InterfacesUtil symfony2Util = new Symfony2InterfacesUtil(); + return !symfony2Util.isInstanceOf(entityClass, repositoryClass); + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryResultTypeProvider.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryResultTypeProvider.java new file mode 100644 index 000000000..6a69b499d --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryResultTypeProvider.java @@ -0,0 +1,61 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine; + +import com.intellij.openapi.project.DumbService; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.resolve.types.PhpType; +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider; +import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + */ +public class ObjectRepositoryResultTypeProvider implements PhpTypeProvider { + + @Nullable + @Override + public PhpType getType(PsiElement e) { + if (DumbService.getInstance(e.getProject()).isDumb()) { + return null; + } + + Symfony2InterfacesUtil interfacesUtil = new Symfony2InterfacesUtil(); + if (!interfacesUtil.isObjectRepositoryCall(e)) { + return null; + } + + MethodReference met = (MethodReference) e; + String methodName = met.getName(); + + // at least one parameter is necessary on some finds + if(null != methodName && !methodName.equals("findAll")) { + PsiElement[] parameters = met.getParameters(); + if(parameters.length == 0) { + return null; + } + } + + // @TODO: find the previously defined type instead of try it on the parameter, we now can rely on it! + // find the called repository name on method before + if(!(met.getFirstChild() instanceof MethodReference)) { + return null; + } + + String repositoryName = Symfony2InterfacesUtil.getFirstArgumentStringValue((MethodReference) met.getFirstChild()); + PhpClass phpClass = EntityHelper.resolveShortcutName(e.getProject(), repositoryName); + + if(phpClass == null) { + return null; + } + + + if(null != methodName && (methodName.equals("findAll") || methodName.equals("findBy"))) { + return new PhpType().add(phpClass.getFQN() + "[]"); + } + + return new PhpType().add(phpClass); + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryTypeProvider.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryTypeProvider.java new file mode 100644 index 000000000..057a47754 --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ObjectRepositoryTypeProvider.java @@ -0,0 +1,45 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine; + +import com.intellij.openapi.project.DumbService; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.resolve.types.PhpType; +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider; +import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + */ +public class ObjectRepositoryTypeProvider implements PhpTypeProvider { + + + @Nullable + @Override + public PhpType getType(PsiElement e) { + if (DumbService.getInstance(e.getProject()).isDumb()) { + return null; + } + + Symfony2InterfacesUtil interfacesUtil = new Symfony2InterfacesUtil(); + if (!interfacesUtil.isGetRepositoryCall(e)) { + return null; + } + + String repositoryName = Symfony2InterfacesUtil.getFirstArgumentStringValue((MethodReference) e); + if (null == repositoryName) { + return null; + } + + // @TODO: parse xml or yml for repositoryClass? + PhpClass phpClass = EntityHelper.resolveShortcutName(e.getProject(), repositoryName + "Repository"); + + if(phpClass == null) { + return null; + } + + return new PhpType().add(phpClass); + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/component/EntityNamesParser.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/component/EntityNamesParser.java new file mode 100644 index 000000000..db4854a1c --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/component/EntityNamesParser.java @@ -0,0 +1,61 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine.component; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.xpath.*; + +/** + * @author Daniel Espendiller + */ +public class EntityNamesParser { + + private DocumentBuilder documentBuilder; + + public EntityNamesParser() throws ParserConfigurationException { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + documentBuilder = dbFactory.newDocumentBuilder(); + } + + public Map parse(InputStream stream) throws IOException, SAXException { + return parse(documentBuilder.parse(stream)); + } + + public Map parse(File file) throws IOException, SAXException { + return parse(documentBuilder.parse(file)); + } + + public Map parse(Document document) { + + Map map = new HashMap(); + + Object result = null; + try { + XPath xpath = XPathFactory.newInstance().newXPath(); + XPathExpression xPathExpr = xpath.compile("/container/services/service[@id[starts-with(.,'doctrine.orm.')]]//call[@method='setEntityNamespaces']//argument[@key]"); + result = xPathExpr.evaluate(document, XPathConstants.NODESET); + } catch (XPathExpressionException e) { + return map; + } + + NodeList nodes = (NodeList) result; + + for (int i = 0; i < nodes.getLength(); i++) { + Element node = (Element) nodes.item(i); + map.put(node.getAttribute("key"), "\\" + node.getTextContent()); + } + + return map; + } + +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/dict/DoctrineEntityLookupElement.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/dict/DoctrineEntityLookupElement.java new file mode 100644 index 000000000..ae5cdc380 --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/dict/DoctrineEntityLookupElement.java @@ -0,0 +1,36 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine.dict; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + */ +public class DoctrineEntityLookupElement extends LookupElement { + + private String entityName; + private PhpClass className; + + public DoctrineEntityLookupElement(String entityName, PhpClass className) { + this.entityName = entityName; + this.className = className; + } + + @NotNull + @Override + public String getLookupString() { + return entityName; + } + + public void renderElement(LookupElementPresentation presentation) { + presentation.setItemText(getLookupString()); + presentation.setTypeText(className.getPresentableFQN()); + presentation.setTypeGrayed(true); + presentation.setIcon(Symfony2Icons.DOCTRINE); + } + +} + diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/EntityNamesParserTest.java b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/EntityNamesParserTest.java new file mode 100644 index 000000000..e2efcfd1a --- /dev/null +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/EntityNamesParserTest.java @@ -0,0 +1,25 @@ +package fr.adrienbrault.idea.symfony2plugin.tests.doctrine.component; + +import fr.adrienbrault.idea.symfony2plugin.doctrine.component.EntityNamesParser; +import org.junit.Assert; +import org.junit.Test; +import java.io.File; +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class EntityNamesParserTest extends Assert { + + @Test + public void testParse() throws Exception { + + File testFile = new File(this.getClass().getResource("appDevDebugProjectContainer.xml").getFile()); + Map map = new EntityNamesParser().parse(testFile); + + assertEquals("\\My\\NiceBundle\\Entity", map.get("MyNiceBundle")); + assertEquals("\\Your\\TestBundle\\Entity", map.get("YourTestBundle")); + } + +} + diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/appDevDebugProjectContainer.xml b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/appDevDebugProjectContainer.xml new file mode 100644 index 000000000..2ee73be4c --- /dev/null +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/component/appDevDebugProjectContainer.xml @@ -0,0 +1,43 @@ + + + + + /EntityManager_516982d26ada1.php + + + + + + + + My\NiceBundle\Entity + + + + + + + + + + + EntityManager_516982d26ada1.php + + + + + + + + Your\TestBundle\Entity + + + + + + + + + + +