package fr.adrienbrault.idea.symfony2plugin.routing; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.ide.highlighter.XmlFileType; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.impl.source.xml.XmlDocumentImpl; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.*; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.indexing.FileBasedIndexImpl; import com.jetbrains.php.lang.PhpFileType; import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.PhpPsiUtil; import com.jetbrains.php.lang.psi.elements.*; import de.espend.idea.php.annotation.dict.PhpDocCommentAnnotation; import de.espend.idea.php.annotation.dict.PhpDocTagAnnotation; import de.espend.idea.php.annotation.util.AnnotationUtil; import fr.adrienbrault.idea.symfony2plugin.Settings; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.extension.RoutingLoader; import fr.adrienbrault.idea.symfony2plugin.extension.RoutingLoaderParameter; import fr.adrienbrault.idea.symfony2plugin.routing.dic.ControllerClassOnShortcutReturn; import fr.adrienbrault.idea.symfony2plugin.routing.dic.ServiceRouteContainer; import fr.adrienbrault.idea.symfony2plugin.routing.dict.RouteInterface; import fr.adrienbrault.idea.symfony2plugin.routing.dict.RoutesContainer; import fr.adrienbrault.idea.symfony2plugin.routing.dict.RoutingFile; import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors; import fr.adrienbrault.idea.symfony2plugin.stubs.dict.StubIndexedRoute; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.AnnotationRoutesStubIndex; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.RoutesStubIndex; import fr.adrienbrault.idea.symfony2plugin.util.AnnotationBackportUtil; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil; import fr.adrienbrault.idea.symfony2plugin.util.controller.ControllerAction; import fr.adrienbrault.idea.symfony2plugin.util.controller.ControllerIndex; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLFileType; import org.jetbrains.yaml.YAMLUtil; import org.jetbrains.yaml.psi.*; import java.io.File; import java.util.*; import java.util.stream.Collectors; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class RouteHelper { private static final Key<CachedValue<Map<String, Route>>> ROUTE_CACHE = new Key<>("SYMFONY:ROUTE_CACHE"); private static Set<String> ROUTE_CLASSES = new HashSet<>(Arrays.asList( "Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Route", "Symfony\\Component\\Routing\\Annotation\\Route" )); public static Map<Project, Map<String, RoutesContainer>> COMPILED_CACHE = new HashMap<>(); private static final ExtensionPointName<RoutingLoader> ROUTING_LOADER = new ExtensionPointName<>( "fr.adrienbrault.idea.symfony2plugin.extension.RoutingLoader" ); public static LookupElement[] getRouteParameterLookupElements(@NotNull Project project, @NotNull String routeName) { List<LookupElement> lookupElements = new ArrayList<>(); Route route = RouteHelper.getRoute(project, routeName); if(route == null) { return lookupElements.toArray(new LookupElement[lookupElements.size()]); } for(String values: route.getVariables()) { lookupElements.add(LookupElementBuilder.create(values).withIcon(Symfony2Icons.ROUTE)); } return lookupElements.toArray(new LookupElement[lookupElements.size()]); } @Nullable public static Route getRoute(@NotNull Project project, @NotNull String routeName) { Map<String, Route> compiledRoutes = RouteHelper.getCompiledRoutes(project); if(compiledRoutes.containsKey(routeName)) { return compiledRoutes.get(routeName); } // @TODO: provide multiple ones Collection<VirtualFile> routeFiles = FileBasedIndex.getInstance().getContainingFiles(RoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project)); for(StubIndexedRoute route: FileBasedIndex.getInstance().getValues(RoutesStubIndex.KEY, routeName, GlobalSearchScope.filesScope(project, routeFiles))) { return new Route(route); } return null; } public static PsiElement[] getRouteParameterPsiElements(Project project, String routeName, String parameterName) { List<PsiElement> results = new ArrayList<>(); for (PsiElement psiElement : RouteHelper.getMethods(project, routeName)) { if(psiElement instanceof Method) { for(Parameter parameter: ((Method) psiElement).getParameters()) { if(parameter.getName().equals(parameterName)) { results.add(parameter); } } } } return results.toArray(new PsiElement[results.size()]); } public static PsiElement[] getMethods(Project project, String routeName) { Route route = getRoute(project, routeName); if(route == null) { return new PsiElement[0]; } String controllerName = route.getController(); return getMethodsOnControllerShortcut(project, controllerName); } /** * convert to controller name to method: * * FooBundle\Controller\BarController::fooBarAction * foo_service_bar:fooBar * AcmeDemoBundle:Demo:hello * * @param project current project * @param controllerName controller service, raw or compiled * @return targets */ @NotNull public static PsiElement[] getMethodsOnControllerShortcut(Project project, String controllerName) { if(controllerName == null) { return new PsiElement[0]; } if(controllerName.contains("::")) { // FooBundle\Controller\BarController::fooBarAction String className = controllerName.substring(0, controllerName.lastIndexOf("::")); String methodName = controllerName.substring(controllerName.lastIndexOf("::") + 2); Method method = PhpElementsUtil.getClassMethod(project, className, methodName); return method != null ? new PsiElement[] {method} : new PsiElement[0]; } else if(controllerName.contains(":")) { // AcmeDemoBundle:Demo:hello String[] split = controllerName.split(":"); if(split.length == 3) { // try to resolve on bundle path SymfonyBundle symfonyBundle = new SymfonyBundleUtil(project).getBundle(split[0]); if(symfonyBundle != null) { // AcmeDemoBundle\Controller\DemoController:helloAction Method method = PhpElementsUtil.getClassMethod(project, symfonyBundle.getNamespaceName() + "Controller\\" + split[1] + "Controller", split[2] + "Action"); if(method != null) { return new PsiElement[] {method}; } } // fallback to controller class instances, if relative path doesnt follow default file structure Method method = ControllerIndex.getControllerMethod(project, controllerName); if(method != null) { return new PsiElement[] {method}; } } // foo_service_bar:fooBar ControllerAction controllerServiceAction = new ControllerIndex(project).getControllerActionOnService(controllerName); if(controllerServiceAction != null) { return new PsiElement[] {controllerServiceAction.getMethod()}; } } return new PsiElement[0]; } /** * convert to controller class: * * FooBundle\Controller\BarController::fooBarAction * foo_service_bar:fooBar * AcmeDemoBundle:Demo:hello * * @param project current project * @param controllerName controller service, raw or compiled * @return targets */ @Nullable public static ControllerClassOnShortcutReturn getControllerClassOnShortcut(@NotNull Project project,@NotNull String controllerName) { if(controllerName.contains("::")) { // FooBundle\Controller\BarController::fooBarAction PhpClass aClass = PhpElementsUtil.getClass(project, controllerName.substring(0, controllerName.lastIndexOf("::"))); if(aClass != null) { return new ControllerClassOnShortcutReturn(aClass); } return null; } // AcmeDemoBundle:Demo:hello String[] split = controllerName.split(":"); if(split.length == 3) { // try to resolve on bundle path SymfonyBundle symfonyBundle = new SymfonyBundleUtil(project).getBundle(split[0]); if(symfonyBundle != null) { PhpClass aClass = PhpElementsUtil.getClass(project, symfonyBundle.getNamespaceName() + "Controller\\" + split[1] + "Controller"); if(aClass != null) { return new ControllerClassOnShortcutReturn(aClass); } } } else if(split.length == 2) { // controller as service: // foo_service_bar:fooBar PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, split[0]); if(phpClass != null) { return new ControllerClassOnShortcutReturn(phpClass, true); } } return null; } private static <E> ArrayList<E> makeCollection(Iterable<E> iter) { ArrayList<E> list = new ArrayList<>(); for (E item : iter) { list.add(item); } return list; } private static String getPath(Project project, String path) { if (!FileUtil.isAbsolute(path)) { // Project relative path path = project.getBasePath() + "/" + path; } return path; } public static Map<String, Route> getCompiledRoutes(@NotNull Project project) { Set<String> files = new HashSet<>(); // old deprecated single file String pathToUrlGenerator = Settings.getInstance(project).pathToUrlGenerator; if(pathToUrlGenerator != null) { files.add(pathToUrlGenerator); } // add custom routing files on settings List<RoutingFile> routingFiles = Settings.getInstance(project).routingFiles; if(routingFiles != null) { for (RoutingFile routingFile : routingFiles) { String path = routingFile.getPath(); if(StringUtils.isNotBlank(path)) { files.add(path); } } } // add defaults; if user never has changed the settings if(routingFiles == null || routingFiles.size() == 0) { Collections.addAll(files, Settings.DEFAULT_ROUTES); } for(String file: files) { File urlGeneratorFile = new File(getPath(project, file)); VirtualFile virtualUrlGeneratorFile = VfsUtil.findFileByIoFile(urlGeneratorFile, false); if (virtualUrlGeneratorFile == null || !urlGeneratorFile.exists()) { // clean file cache if(COMPILED_CACHE.containsKey(project) && COMPILED_CACHE.get(project).containsKey(file)) { COMPILED_CACHE.get(project).remove(file); } } else { if(!COMPILED_CACHE.containsKey(project)) { COMPILED_CACHE.put(project, new HashMap<>()); } Long routesLastModified = urlGeneratorFile.lastModified(); if(!COMPILED_CACHE.get(project).containsKey(file) || !COMPILED_CACHE.get(project).get(file).getLastMod().equals(routesLastModified)) { COMPILED_CACHE.get(project).put(file, new RoutesContainer( routesLastModified, RouteHelper.getRoutesInsideUrlGeneratorFile(project, virtualUrlGeneratorFile) )); Symfony2ProjectComponent.getLogger().info("update routing: " + urlGeneratorFile.toString()); } } } Map<String, Route> routes = new HashMap<>(); if(COMPILED_CACHE.containsKey(project)) { for (RoutesContainer container : COMPILED_CACHE.get(project).values()) { routes.putAll(container.getRoutes()); } } RoutingLoaderParameter parameter = null; for (RoutingLoader routingLoader : ROUTING_LOADER.getExtensions()) { if(parameter == null) { parameter = new RoutingLoaderParameter(project, routes); } routingLoader.invoke(parameter); } return routes; } @NotNull public static Map<String, Route> getRoutesInsideUrlGeneratorFile(@NotNull Project project, @NotNull VirtualFile virtualFile) { PsiFile psiFile = PsiElementUtils.virtualFileToPsiFile(project, virtualFile); if(!(psiFile instanceof PhpFile)) { return Collections.emptyMap(); } return getRoutesInsideUrlGeneratorFile(psiFile); } /** * Temporary or remote files dont support "isInstanceOf", check for string implementation first */ private static boolean isRouteClass(@NotNull PhpClass phpClass) { for (ClassReference classReference : phpClass.getExtendsList().getReferenceElements()) { String fqn = classReference.getFQN(); if(fqn != null && StringUtils.stripStart(fqn, "\\").equalsIgnoreCase("Symfony\\Component\\Routing\\Generator\\UrlGenerator")) { return true; } } for (PhpClass phpInterface : phpClass.getImplementedInterfaces()) { String fqn = phpInterface.getFQN(); if( StringUtils.stripStart(fqn, "\\").equalsIgnoreCase("Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface")) { return true; } } return PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface"); } @NotNull public static Map<String, Route> getRoutesInsideUrlGeneratorFile(@NotNull PsiFile psiFile) { Map<String, Route> routes = new HashMap<>(); // heavy stuff here, to get nested routing array :) // list($variables, $defaults, $requirements, $tokens, $hostTokens) Collection<PhpClass> phpClasses = PsiTreeUtil.findChildrenOfType(psiFile, PhpClass.class); for(PhpClass phpClass: phpClasses) { if(!isRouteClass(phpClass)) { continue; } // Symfony < 2.8 // static private $declaredRoutes = array(...) for(Field field: phpClass.getFields()) { if(!field.getName().equals("declaredRoutes")) { continue; } PsiElement defaultValue = field.getDefaultValue(); if(!(defaultValue instanceof ArrayCreationExpression)) { continue; } collectRoutesOnArrayCreation(routes, (ArrayCreationExpression) defaultValue); } // Symfony >= 2.8 // if (null === self::$declaredRoutes) { // self::$declaredRoutes = array() // } Method constructor = phpClass.getConstructor(); if(constructor == null) { continue; } for (FieldReference fieldReference : PsiTreeUtil.collectElementsOfType(constructor, FieldReference.class)) { String canonicalText = fieldReference.getCanonicalText(); if(!"declaredRoutes".equals(canonicalText)) { continue; } PsiElement assignExpression = fieldReference.getParent(); if(!(assignExpression instanceof AssignmentExpression)) { continue; } PhpPsiElement value = ((AssignmentExpression) assignExpression).getValue(); if(!(value instanceof ArrayCreationExpression)) { continue; } collectRoutesOnArrayCreation(routes, (ArrayCreationExpression) value); } } return routes; } /** * Collects routes in: * * array( * _wdt' => array(..) * } * */ private static void collectRoutesOnArrayCreation(@NotNull Map<String, Route> routes, @NotNull ArrayCreationExpression defaultValue) { for(ArrayHashElement arrayHashElement: defaultValue.getHashElements()) { PsiElement hashKey = arrayHashElement.getKey(); if(!(hashKey instanceof StringLiteralExpression)) { continue; } String routeName = ((StringLiteralExpression) hashKey).getContents(); if(!isProductionRouteName(routeName)) { continue; } routeName = convertLanguageRouteName(routeName); PsiElement hashValue = arrayHashElement.getValue(); if(hashValue instanceof ArrayCreationExpression) { routes.put(routeName, convertRouteConfig(routeName, (ArrayCreationExpression) hashValue)); } } } private static Route convertRouteConfig(String routeName, ArrayCreationExpression hashValue) { List<ArrayHashElement> hashElementCollection = makeCollection(hashValue.getHashElements()); HashSet<String> variables = new HashSet<>(); if(hashElementCollection.size() >= 1 && hashElementCollection.get(0).getValue() instanceof ArrayCreationExpression) { variables.addAll(PhpElementsUtil.getArrayKeyValueMap((ArrayCreationExpression) hashElementCollection.get(0).getValue()).values()); } HashMap<String, String> defaults = new HashMap<>(); if(hashElementCollection.size() >= 2 && hashElementCollection.get(1).getValue() instanceof ArrayCreationExpression) { defaults = PhpElementsUtil.getArrayKeyValueMap((ArrayCreationExpression) hashElementCollection.get(1).getValue()); } HashMap<String, String>requirements = new HashMap<>(); if(hashElementCollection.size() >= 3 && hashElementCollection.get(2).getValue() instanceof ArrayCreationExpression) { requirements = PhpElementsUtil.getArrayKeyValueMap((ArrayCreationExpression) hashElementCollection.get(2).getValue()); } ArrayList<Collection<String>> tokens = new ArrayList<>(); if(hashElementCollection.size() >= 4 && hashElementCollection.get(3).getValue() instanceof ArrayCreationExpression) { ArrayCreationExpression tokenArray = (ArrayCreationExpression) hashElementCollection.get(3).getValue(); if(tokenArray != null) { for(ArrayHashElement tokenArrayConfig: tokenArray.getHashElements()) { if(tokenArrayConfig.getValue() instanceof ArrayCreationExpression) { HashMap<String, String> arrayKeyValueMap = PhpElementsUtil.getArrayKeyValueMap((ArrayCreationExpression) tokenArrayConfig.getValue()); tokens.add(arrayKeyValueMap.values()); } } } } // hostTokens = 4 need them? return new Route(routeName, variables, defaults, requirements, tokens); } private static boolean isProductionRouteName(String routeName) { return !routeName.matches("_assetic_[0-9a-z]+[_\\d+]*"); } /** * support I18nRoutingBundle */ private static String convertLanguageRouteName(String routeName) { if(routeName.matches("^[a-z]{2}__RG__.*$")) { routeName = routeName.replaceAll("^[a-z]{2}+__RG__", ""); } return routeName; } /** * Foo\Bar::methodAction */ @Nullable private static String convertMethodToRouteControllerName(@NotNull Method method) { PhpClass phpClass = method.getContainingClass(); if(phpClass == null) { return null; } return StringUtils.stripStart(phpClass.getFQN(), "\\") + "::" + method.getName(); } /** * FooBundle:Bar::method * FooBundle:Bar\\Foo::method */ @Nullable public static String convertMethodToRouteShortcutControllerName(@NotNull Method method) { PhpClass phpClass = method.getContainingClass(); if(phpClass == null) { return null; } String className = StringUtils.stripStart(phpClass.getFQN(), "\\"); int bundlePos = className.lastIndexOf("Bundle\\"); if(bundlePos == -1) { return null; } SymfonyBundle symfonyBundle = new SymfonyBundleUtil(method.getProject()).getContainingBundle(phpClass); if(symfonyBundle == null) { return null; } String name = method.getName(); String methodName = name.substring(0, name.length() - "Action".length()); // try to to find relative class name String controllerClass = className.toLowerCase(); String bundleClass = StringUtils.stripStart(symfonyBundle.getNamespaceName(), "\\").toLowerCase(); if(!controllerClass.startsWith(bundleClass)) { return null; } String relative = StringUtils.stripStart(phpClass.getFQN(), "\\").substring(bundleClass.length()); if(relative.startsWith("Controller\\")) { relative = relative.substring("Controller\\".length()); } if(relative.endsWith("Controller")) { relative = relative.substring(0, relative.length() - "Controller".length()); } return String.format("%s:%s:%s", symfonyBundle.getName(), relative.replace("/", "\\"), methodName); } @NotNull private static Collection<VirtualFile> getRouteDefinitionInsideFile(@NotNull Project project, @NotNull String... routeNames) { Collection<VirtualFile> virtualFiles = new ArrayList<>(); FileBasedIndexImpl.getInstance().getFilesWithKey(RoutesStubIndex.KEY, new HashSet<>(Arrays.asList(routeNames)), virtualFile -> { virtualFiles.add(virtualFile); return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), YAMLFileType.YML, XmlFileType.INSTANCE)); FileBasedIndexImpl.getInstance().getFilesWithKey(AnnotationRoutesStubIndex.KEY, new HashSet<>(Arrays.asList(routeNames)), virtualFile -> { virtualFiles.add(virtualFile); return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)); return virtualFiles; } @NotNull public static Collection<StubIndexedRoute> getYamlRouteDefinitions(@NotNull YAMLDocument yamlDocument) { Collection<StubIndexedRoute> indexedRoutes = new ArrayList<>(); for(YAMLKeyValue yamlKeyValue : YamlHelper.getTopLevelKeyValues((YAMLFile) yamlDocument.getContainingFile())) { YAMLValue element = yamlKeyValue.getValue(); YAMLKeyValue path = YAMLUtil.findKeyInProbablyMapping(element, "path"); // Symfony bc if(path == null) { path = YAMLUtil.findKeyInProbablyMapping(element, "pattern"); } if(path == null) { continue; } // cleanup: 'foo', "foo" String keyText = StringUtils.strip(StringUtils.strip(yamlKeyValue.getKeyText(), "'"), "\""); if(StringUtils.isBlank(keyText)) { continue; } StubIndexedRoute route = new StubIndexedRoute(keyText); String routePath = path.getValueText(); if(StringUtils.isNotBlank(routePath)) { route.setPath(routePath); } String methods = YamlHelper.getStringValueOfKeyInProbablyMapping(element, "methods"); if(methods != null) { // value: [GET, POST, String[] split = methods.replace("[", "").replace("]", "").replaceAll(" +", "").toLowerCase().split(","); if(split.length > 0) { route.addMethod(split); } } String controller = getYamlController(yamlKeyValue); if(controller != null) { route.setController(normalizeRouteController(controller)); } indexedRoutes.add(route); } return indexedRoutes; } public static Collection<StubIndexedRoute> getXmlRouteDefinitions(XmlFile psiFile) { XmlDocumentImpl document = PsiTreeUtil.getChildOfType(psiFile, XmlDocumentImpl.class); if(document == null) { return Collections.emptyList(); } Collection<StubIndexedRoute> indexedRoutes = new ArrayList<>(); /** * <routes> * <route id="foo" path="/blog/{slug}" methods="GET"> * <default key="_controller">Foo</default> * </route> * </routes> */ for(XmlTag xmlTag: PsiTreeUtil.getChildrenOfTypeAsList(psiFile.getFirstChild(), XmlTag.class)) { if(xmlTag.getName().equals("routes")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("route")) { XmlAttribute xmlAttribute = servicesTag.getAttribute("id"); if(xmlAttribute != null) { String attrValue = xmlAttribute.getValue(); if(StringUtils.isNotBlank(attrValue)) { StubIndexedRoute e = new StubIndexedRoute(attrValue); String pathAttribute = servicesTag.getAttributeValue("path"); if(pathAttribute == null) { pathAttribute = servicesTag.getAttributeValue("pattern"); } if(pathAttribute != null && StringUtils.isNotBlank(pathAttribute) ) { e.setPath(pathAttribute); } String methods = servicesTag.getAttributeValue("methods"); if(methods != null && StringUtils.isNotBlank(methods)) { String[] split = methods.replaceAll(" +", "").toLowerCase().split("\\|"); if(split.length > 0) { e.addMethod(split); } } for(XmlTag subTag :servicesTag.getSubTags()) { if("default".equalsIgnoreCase(subTag.getName())) { String keyValue = subTag.getAttributeValue("key"); if(keyValue != null && "_controller".equals(keyValue)) { String actionName = subTag.getValue().getTrimmedText(); if(StringUtils.isNotBlank(actionName)) { e.setController(normalizeRouteController(actionName)); } } } } indexedRoutes.add(e); } } } } } } return indexedRoutes; } @Nullable private static String getYamlController(YAMLKeyValue psiElement) { /* * foo: * defaults: { _controller: "Bundle:Foo:Bar" } * defaults: * _controller: "Bundle:Foo:Bar" */ YAMLKeyValue yamlKeyValue = YamlHelper.getYamlKeyValue(psiElement, "defaults"); if(yamlKeyValue != null) { final YAMLValue container = yamlKeyValue.getValue(); if(container instanceof YAMLMapping) { YAMLKeyValue yamlKeyValueController = YamlHelper.getYamlKeyValue(container, "_controller", true); if(yamlKeyValueController != null) { String valueText = yamlKeyValueController.getValueText(); if(StringUtils.isNotBlank(valueText)) { return valueText; } } } } return null; } @Nullable public static PsiElement getXmlRouteNameTarget(@NotNull XmlFile psiFile,@NotNull String routeName) { XmlDocumentImpl document = PsiTreeUtil.getChildOfType(psiFile, XmlDocumentImpl.class); if(document == null) { return null; } for(XmlTag xmlTag: PsiTreeUtil.getChildrenOfTypeAsList(psiFile.getFirstChild(), XmlTag.class)) { if(xmlTag.getName().equals("routes")) { for(XmlTag routeTag: xmlTag.getSubTags()) { if(routeTag.getName().equals("route")) { XmlAttribute xmlAttribute = routeTag.getAttribute("id"); if(xmlAttribute != null) { String attrValue = xmlAttribute.getValue(); if(routeName.equals(attrValue)) { return xmlAttribute; } } } } } } return null; } public static boolean isServiceController(@NotNull String shortcutName) { return !shortcutName.contains("::") && shortcutName.contains(":") && !shortcutName.contains("\\") && shortcutName.split(":").length == 2; } @NotNull public static List<Route> getRoutesOnControllerAction(@NotNull Method method) { Set<String> routeNames = new HashSet<>(); ContainerUtil.addIfNotNull(routeNames, RouteHelper.convertMethodToRouteControllerName(method)); ContainerUtil.addIfNotNull(routeNames, RouteHelper.convertMethodToRouteShortcutControllerName(method)); Map<String, Route> allRoutes = getAllRoutes(method.getProject()); List<Route> routes = new ArrayList<>(); // resolve indexed routes if(routeNames.size() > 0) { routes.addAll(allRoutes.values().stream() .filter(route -> route.getController() != null && routeNames.contains(route.getController())) .collect(Collectors.toList()) ); } // search for services routes.addAll( ServiceRouteContainer.build(allRoutes).getMethodMatches(method) ); return routes; } /** * Find every possible route name declaration inside yaml, xml or @Route annotation */ @Nullable public static PsiElement getRouteNameTarget(@NotNull Project project, @NotNull String routeName) { for(VirtualFile virtualFile: RouteHelper.getRouteDefinitionInsideFile(project, routeName)) { PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); if(psiFile instanceof YAMLFile) { return YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, routeName); } else if(psiFile instanceof XmlFile) { PsiElement target = RouteHelper.getXmlRouteNameTarget((XmlFile) psiFile, routeName); if(target != null) { return target; } } else if(psiFile instanceof PhpFile) { // find on @Route annotation for (PhpClass phpClass : PhpPsiUtil.findAllClasses((PhpFile) psiFile)) { for (Method method : phpClass.getOwnMethods()) { PhpDocComment docComment = method.getDocComment(); if(docComment == null) { continue; } PhpDocCommentAnnotation container = AnnotationUtil.getPhpDocCommentAnnotationContainer(docComment); if(container == null) { continue; } // multiple @Route annotation in bundles are allowed for (String routeClass : ROUTE_CLASSES) { PhpDocTagAnnotation phpDocTagAnnotation = container.getPhpDocBlock(routeClass); if(phpDocTagAnnotation != null) { String annotationRouteName = phpDocTagAnnotation.getPropertyValue("name"); if(annotationRouteName != null) { // name provided @Route(name="foobar") if(routeName.equals(annotationRouteName)) { return phpDocTagAnnotation.getPropertyValuePsi("name"); } } else { // just @Route() without name provided String routeByMethod = AnnotationBackportUtil.getRouteByMethod(phpDocTagAnnotation.getPhpDocTag()); if(routeName.equals(routeByMethod)) { return phpDocTagAnnotation.getPhpDocTag(); } } } } } } } } return null; } @Nullable public static String getRouteUrl(Route route) { if(route.getPath() != null) { return route.getPath(); } String url = ""; // copy list; List<Collection<String>> tokens = new ArrayList<>(route.getTokens()); Collections.reverse(tokens); for(Collection<String> token: tokens) { // copy, we are not allowed to mod list List<String> list = new ArrayList<>(token); if(list.size() >= 2 && list.get(1).equals("text")) { url = url.concat(list.get(0)); } if(list.size() >= 4 && list.get(3).equals("variable")) { url = url.concat(list.get(2) + "{" + list.get(0) + "}"); } } return url.length() == 0 ? null : url; } public static List<LookupElement> getRoutesLookupElements(final @NotNull Project project) { Map<String, Route> routes = RouteHelper.getCompiledRoutes(project); final List<LookupElement> lookupElements = new ArrayList<>(); final Set<String> uniqueSet = new HashSet<>(); for (Route route : routes.values()) { lookupElements.add(new RouteLookupElement(route)); uniqueSet.add(route.getName()); } SymfonyProcessors.CollectProjectUniqueKeysStrong ymlProjectProcessor = new SymfonyProcessors.CollectProjectUniqueKeysStrong(project, RoutesStubIndex.KEY, uniqueSet); FileBasedIndex.getInstance().processAllKeys(RoutesStubIndex.KEY, ymlProjectProcessor, project); for(String routeName: ymlProjectProcessor.getResult()) { if(uniqueSet.contains(routeName)) { continue; } for(StubIndexedRoute route: FileBasedIndex.getInstance().getValues(RoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))) { lookupElements.add(new RouteLookupElement(new Route(route), true)); uniqueSet.add(routeName); } } SymfonyProcessors.CollectProjectUniqueKeysStrong annotationProjectProcessor = new SymfonyProcessors.CollectProjectUniqueKeysStrong(project, AnnotationRoutesStubIndex.KEY, uniqueSet); FileBasedIndex.getInstance().processAllKeys(AnnotationRoutesStubIndex.KEY, annotationProjectProcessor, project); for(String routeName: annotationProjectProcessor.getResult()) { if(uniqueSet.contains(routeName)) { continue; } RouteInterface firstItem = ContainerUtil.getFirstItem(FileBasedIndexImpl.getInstance().getValues(AnnotationRoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))); if(firstItem != null) { lookupElements.add(new RouteLookupElement(new Route(firstItem), true)); uniqueSet.add(routeName); } } return lookupElements; } @NotNull public static List<PsiElement> getRouteDefinitionTargets(Project project, String routeName) { List<PsiElement> targets = new ArrayList<>(); Collections.addAll(targets, RouteHelper.getMethods(project, routeName)); PsiElement yamlKey = RouteHelper.getRouteNameTarget(project, routeName); if(yamlKey != null) { targets.add(yamlKey); } return targets; } @NotNull synchronized public static Map<String, Route> getAllRoutes(final @NotNull Project project) { CachedValue<Map<String, Route>> cache = project.getUserData(ROUTE_CACHE); if (cache == null) { cache = CachedValuesManager.getManager(project).createCachedValue(() -> CachedValueProvider.Result.create(getAllRoutesProxy(project), PsiModificationTracker.MODIFICATION_COUNT), false ); project.putUserData(ROUTE_CACHE, cache); } return cache.getValue(); } @NotNull private static Map<String, Route> getAllRoutesProxy(@NotNull Project project) { Map<String, Route> routes = new HashMap<>(); routes.putAll(RouteHelper.getCompiledRoutes(project)); Set<String> uniqueKeySet = new HashSet<>(routes.keySet()); SymfonyProcessors.CollectProjectUniqueKeysStrong ymlProjectProcessor = new SymfonyProcessors.CollectProjectUniqueKeysStrong(project, RoutesStubIndex.KEY, uniqueKeySet); FileBasedIndex.getInstance().processAllKeys(RoutesStubIndex.KEY, ymlProjectProcessor, project); for(String routeName: ymlProjectProcessor.getResult()) { if(uniqueKeySet.contains(routeName)) { continue; } for(StubIndexedRoute route: FileBasedIndex.getInstance().getValues(RoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))) { uniqueKeySet.add(routeName); routes.put(routeName, new Route(route)); } } SymfonyProcessors.CollectProjectUniqueKeysStrong annotationProjectProcessor = new SymfonyProcessors.CollectProjectUniqueKeysStrong(project, AnnotationRoutesStubIndex.KEY, uniqueKeySet); FileBasedIndex.getInstance().processAllKeys(AnnotationRoutesStubIndex.KEY, annotationProjectProcessor, project); for(String routeName: annotationProjectProcessor.getResult()) { if(uniqueKeySet.contains(routeName)) { continue; } RouteInterface firstItem = ContainerUtil.getFirstItem(FileBasedIndexImpl.getInstance().getValues(AnnotationRoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))); if(firstItem != null) { routes.put(routeName, new Route(firstItem)); } } return routes; } @NotNull private static String normalizeRouteController(@NotNull String string) { return string.replace("/", "\\"); } /** * Support "use Symfony\Component\Routing\Annotation\Route as BaseRoute;" */ public static boolean isRouteClassAnnotation(@NotNull String clazz) { String myClazz = StringUtils.stripStart(clazz, "\\"); return ROUTE_CLASSES.stream().anyMatch(s -> s.equalsIgnoreCase(myClazz)); } }