Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@

<extensions defaultExtensionNs="com.intellij">
<php.typeProvider implementation="fr.adrienbrault.idea.symfony2plugin.dic.SymfonyContainerTypeProvider"/>
<php.typeProvider implementation="fr.adrienbrault.idea.symfony2plugin.doctrine.ObjectRepositoryTypeProvider"/>
<php.typeProvider implementation="fr.adrienbrault.idea.symfony2plugin.doctrine.ObjectRepositoryResultTypeProvider"/>
<psi.referenceContributor implementation="fr.adrienbrault.idea.symfony2plugin.dic.ServiceReferenceContributor"/>
<psi.referenceContributor implementation="fr.adrienbrault.idea.symfony2plugin.templating.PhpTemplateReferenceContributor"/>
<psi.referenceContributor implementation="fr.adrienbrault.idea.symfony2plugin.doctrine.DoctrineEntityReferenceContributor"/>
<!--<psi.referenceContributor implementation="fr.adrienbrault.idea.symfony2plugin.templating.TwigTemplateReferenceContributor"/>-->
<psi.referenceContributor implementation="fr.adrienbrault.idea.symfony2plugin.routing.PhpRouteReferenceContributor"/>
<completion.contributor language="Twig" implementationClass="fr.adrienbrault.idea.symfony2plugin.templating.TwigTemplateCompletionContributor"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also support \\Doctrine\\Common\\Persistence\\ObjectManager::getRepository for people getting the repository from the EntityManager

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 });
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,6 +37,9 @@ public class Symfony2ProjectComponent implements ProjectComponent {
private Map<String, Route> routes;
private Long routesLastModified;

private Long entityNamespacesMapLastModified;
private Map<String, String> entityNamespaces;

public Symfony2ProjectComponent(Project project) {
this.project = project;
}
Expand Down Expand Up @@ -167,4 +171,32 @@ private String getPath(Project project, String path) {
return path;
}

public Map<String, String> getEntityNamespacesMap() {

String defaultServiceMapFilePath = getPath(project, Settings.getInstance(project).pathToProjectContainer);

File xmlFile = new File(defaultServiceMapFilePath);
if (!xmlFile.exists()) {
return new HashMap<String, String>();
}

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<String, String>();
}

}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
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) };
}
}
);
}

}
67 changes: 67 additions & 0 deletions src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
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<String, String> 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<PhpClass> entity_classes = phpIndex.getClassesByFQN(entity_name);
if(!entity_classes.isEmpty()){
return entity_classes.iterator().next();
}

return null;
}

}
109 changes: 109 additions & 0 deletions src/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityReference.java
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
public class EntityReference extends PsiReferenceBase<PsiElement> 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<String, String> entityNamespaces = symfony2ProjectComponent.getEntityNamespacesMap();

List<LookupElement> results = new ArrayList<LookupElement>();

// find Repository interface to filter RepositoryClasses out
PhpClass repositoryClass = getRepositoryClass(phpIndex);
if(null == repositoryClass) {
return results.toArray();
}

for (Map.Entry<String, String> entry : entityNamespaces.entrySet()) {

// search for classes that match the symfony2 namings
Collection<PhpNamespace> entities = phpIndex.getNamespacesByName(entry.getValue());

// @TODO: it looks like PhpIndex cant search for classes like \ns\Path\*\...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing / with \ should work, right ?

// 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<PhpClass> classes = phpIndex.getInterfacesByFQN("\\Doctrine\\Common\\Persistence\\ObjectRepository");
return classes.isEmpty() ? null : classes.iterator().next();
}

@Nullable
protected PhpClass getClass(PhpIndex phpIndex, String className) {
Collection<PhpClass> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
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);
}

}
Loading