package fr.adrienbrault.idea.symfony2plugin.util.controller; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.project.Project; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.routing.Route; import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; import fr.adrienbrault.idea.symfony2plugin.routing.dic.ServiceRouteContainer; import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PhpIndexUtil; import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * @author Daniel Espendiller <[email protected]> */ public class ControllerIndex { @NotNull final private Project project; private ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector; public ControllerIndex(@NotNull Project project) { this.project = project; } @NotNull public Collection<ControllerAction> getActions() { Collection<ControllerAction> actions = new ArrayList<>(); for (SymfonyBundle symfonyBundle : new SymfonyBundleUtil(project).getBundles()) { actions.addAll(this.getActionMethods(symfonyBundle)); } return actions; } @Nullable public ControllerAction getControllerActionOnService(String shortcutName) { // only foo_bar:Method is valid if(!RouteHelper.isServiceController(shortcutName)) { return null; } String serviceId = shortcutName.substring(0, shortcutName.lastIndexOf(":")); String methodName = shortcutName.substring(shortcutName.lastIndexOf(":") + 1); PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(this.project, serviceId, getLazyServiceCollector(this.project)); if(phpClass == null) { return null; } Method method = phpClass.findMethodByName(methodName); if(method == null) { return null; } return new ControllerAction(serviceId, method); } @NotNull private Collection<ControllerAction> getActionMethods(@NotNull SymfonyBundle symfonyBundle) { String namespaceName = symfonyBundle.getNamespaceName(); if(!namespaceName.startsWith("\\")) { namespaceName = "\\" + namespaceName; } if(!namespaceName.endsWith("\\")) { namespaceName += "\\"; } namespaceName += "Controller"; List<ControllerAction> actions = new ArrayList<>(); for (PhpClass phpClass : PhpIndexUtil.getPhpClassInsideNamespace(this.project, namespaceName)) { if(!phpClass.getName().endsWith("Controller")) { continue; } String presentableFQN = phpClass.getPresentableFQN(); if(presentableFQN.contains("\\Test\\") || presentableFQN.contains("\\Tests\\") || presentableFQN.startsWith("Test\\") || presentableFQN.startsWith("\\Test\\")) { continue; } if(!presentableFQN.startsWith("\\")) { presentableFQN = "\\" + presentableFQN; } presentableFQN = presentableFQN.substring(0, presentableFQN.length() - "Controller".length()); if(presentableFQN.length() == 0) { continue; } String ns = presentableFQN.substring(namespaceName.length() + 1); for(Method method : phpClass.getMethods()) { String methodName = method.getName(); if(!method.getAccess().isPublic() || (method.getName().startsWith("__") && !method.getName().equals("__invoke"))) { continue; } if(methodName.endsWith("Action")) { String shortcutName = symfonyBundle.getName() + ":" + ns.replace("/", "\\") + ':' + methodName.substring(0, methodName.length() - 6); actions.add(new ControllerAction(shortcutName, method)); } String shortcutName = StringUtils.stripStart(phpClass.getPresentableFQN(), "\\"); if(methodName.equals("__invoke")) { actions.add(new ControllerAction(shortcutName, method)); } else { actions.add(new ControllerAction(shortcutName + "::" + method.getName(), method)); } } } return actions; } @NotNull private Collection<ControllerAction> getServiceActionMethods(@NotNull Project project) { Map<String,Route> routes = RouteHelper.getAllRoutes(project); if(routes.size() == 0) { return Collections.emptyList(); } // there is now way to find service controllers directly, // so we search for predefined service controller and use the public methods ContainerCollectionResolver.LazyServiceCollector collector = new ContainerCollectionResolver.LazyServiceCollector(project); Collection<ControllerAction> actions = new ArrayList<>(); for (String serviceName : ServiceRouteContainer.build(routes).getServiceNames()) { PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, serviceName, collector); if(phpClass == null) { continue; } // find public method of the service class which are possible Actions for(Method method : phpClass.getMethods()) { if(method.getAccess().isPublic() && !method.getName().startsWith("__") && !method.getName().startsWith("set")) { actions.add(new ControllerAction(serviceName + ":" + method.getName(), method)); } } } return actions; } @NotNull public Collection<Method> resolveShortcutName(@NotNull String controllerName) { String[] split = controllerName.split(":"); // normalize: "FooBundle:Apple/Bar:foo" => FooBundle:Apple\Bar:foo // support: "FooBundle:Apple\Bar:foo" => FooBundle:Apple\Bar:foo\bar if(split.length == 3) { // normalize incoming path "/" => "\" this are PHP namespace but both supported split[1] = split[1].replaceAll("/+", "\\\\").replaceAll("\\\\+", "\\\\"); split[2] = split[2].replaceAll("/+", "\\\\").replaceAll("\\\\+", "\\\\"); Collection<Method> methods = new HashSet<>(); for (SymfonyBundle symfonyBundle : new SymfonyBundleUtil(project).getBundles()) { // Bundle matched "AppBundle" if(split[0].equalsIgnoreCase(symfonyBundle.getName())) { String namespace = split[1] + "\\" + split[2]; // last element is our method name int lastBackslash = namespace.lastIndexOf("\\"); if(lastBackslash > 0) { String methodName = namespace.substring(lastBackslash + 1); // AppBundle/Controller/FooController String className = symfonyBundle.getNamespaceName() + "Controller\\" + namespace.substring(0, lastBackslash) + "Controller"; for (PhpClass phpClass : PhpElementsUtil.getClassesInterface(project, className)) { // cleanup action to support "fooAction" and "foo" methods if(methodName.endsWith("Action")) { methodName = methodName.substring(0, methodName.length() - "Action".length()); } // find method for (String string : new String[] {methodName, methodName + "Action"}) { Method methodByName = phpClass.findMethodByName(string); if(methodByName != null) { methods.add(methodByName); } } } } } } return methods; } ControllerAction controllerAction = new ControllerIndex(project).getControllerActionOnService(controllerName); if(controllerAction != null) { return Collections.singletonList(controllerAction.getMethod()); } return Collections.emptyList(); } private ContainerCollectionResolver.LazyServiceCollector getLazyServiceCollector(Project project) { return this.lazyServiceCollector == null ? this.lazyServiceCollector = new ContainerCollectionResolver.LazyServiceCollector(project) : this.lazyServiceCollector; } static public List<LookupElement> getControllerLookupElements(Project project) { List<LookupElement> lookupElements = new ArrayList<>(); ControllerIndex controllerIndex = new ControllerIndex(project); for(ControllerAction controllerAction: controllerIndex.getActions()) { lookupElements.add(new ControllerActionLookupElement(controllerAction)); } for(ControllerAction controllerAction: controllerIndex.getServiceActionMethods(project)) { lookupElements.add(new ControllerActionLookupElement(controllerAction)); } return lookupElements; } @NotNull static public Collection<Method> getControllerMethod(@NotNull Project project, @NotNull String controllerName) { return new ControllerIndex(project).resolveShortcutName(controllerName); } }