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

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 @@ -109,8 +109,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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 All @@ -32,6 +33,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 @@ -138,4 +142,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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpNamespace;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
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> em = symfony2ProjectComponent.getEntityNamespacesMap();

List<LookupElement> results = new ArrayList<LookupElement>();
for (String shortcutName : em.keySet()) {

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

// @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 = shortcutName + ':' + className;

// dont add Repository classes and abstract entities
if(!className.endsWith("Repository") && !className.equals("Repository")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the user has an entity named Repository because he is managing a Repository ?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you really want to exclude the repositories, maybe using Symfony2InterfacesUtil.isInstanceOf would work

for (PhpClass entityClass : phpIndex.getClassesByFQN(em.get(shortcutName) + "\\" + className)) {
if(!entityClass.isAbstract()) {
results.add(new DoctrineEntityLookupElement(repoName, entityClass));
}
}
}

}

}

return results.toArray();
}

}
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);
}

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

Choose a reason for hiding this comment

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

I wouldn't concatenate "Repository" to repositoryName here, it is confusing. I'd assign the result to repositoryName above.

repositoryName = repositoryName + "Repository"; 

if(phpClass == null) {
return null;
}

return new PhpType().add(phpClass);
}

}
Loading