diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/Settings.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/Settings.java index 3c43d4da0..9c03f6fe7 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/Settings.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/Settings.java @@ -30,6 +30,7 @@ public class Settings implements PersistentStateComponent { "var/cache/dev/appDevDebugProjectContainer.xml", "var/cache/dev/srcDevDebugProjectContainer.xml", "var/cache/dev/srcApp_KernelDevDebugContainer.xml", + "var/cache/dev/App_KernelDevDebugContainer.xml" // Symfony => 4 + flex }; // Default Symfony 2, 3 and 4 paths @@ -38,6 +39,7 @@ public class Settings implements PersistentStateComponent { "var/cache/dev/appDevUrlGenerator.php", "var/cache/dev/appDevDebugProjectContainerUrlGenerator.php", "var/cache/dev/srcDevDebugProjectContainerUrlGenerator.php", + "var/cache/dev/url_matching_routes.php", // Symfony >= 4 }; public static String DEFAULT_TRANSLATION_PATH = "app/cache/dev/translations"; diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java index c5a4cc723..47f591f87 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java @@ -247,14 +247,6 @@ public static ControllerClassOnShortcutReturn getControllerClassOnShortcut(@NotN return null; } - private static ArrayList makeCollection(Iterable iter) { - ArrayList 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; @@ -344,7 +336,6 @@ public static Map getRoutesInsideUrlGeneratorFile(@NotNull Projec return getRoutesInsideUrlGeneratorFile(psiFile); } - /** * Temporary or remote files dont support "isInstanceOf", check for string implementation first */ @@ -368,9 +359,30 @@ private static boolean isRouteClass(@NotNull PhpClass phpClass) { @NotNull public static Map getRoutesInsideUrlGeneratorFile(@NotNull PsiFile psiFile) { - Map routes = new HashMap<>(); + // Symfony >= 4 + // extract the routes on a return statement + // return [['route'] => [...]] + for (PhpReturn phpReturn : PsiTreeUtil.findChildrenOfType(psiFile, PhpReturn.class)) { + PsiElement argument = phpReturn.getArgument(); + if (!(argument instanceof ArrayCreationExpression)) { + continue; + } + + // get only the inside arrays + // [[..], [..]] => [..], [..] + for (Map.Entry routeArray : PhpElementsUtil.getArrayKeyValueMapWithValueAsPsiElement((ArrayCreationExpression) argument).entrySet()) { + List routeArrayOptions = new ArrayList<>(); + for (PhpPsiElement routeOption : PsiTreeUtil.getChildrenOfTypeAsList(routeArray.getValue(), PhpPsiElement.class)) { + routeArrayOptions.add(PsiTreeUtil.getChildOfType(routeOption, ArrayCreationExpression.class)); + } + + routes.put(routeArray.getKey(), convertRouteConfigForReturnArray(routeArray.getKey(), routeArrayOptions)); + } + } + + // Symfony < 4 // heavy stuff here, to get nested routing array :) // list($variables, $defaults, $requirements, $tokens, $hostTokens) Collection phpClasses = PsiTreeUtil.findChildrenOfType(psiFile, PhpClass.class); @@ -454,9 +466,60 @@ private static void collectRoutesOnArrayCreation(@NotNull Map rou } } + /** + * Used in Symfony > 4 where routes are wrapped into a return array + */ + @NotNull + private static Route convertRouteConfigForReturnArray(@NotNull String routeName, @NotNull List hashElementCollection) { + Set variables = new HashSet<>(); + if(hashElementCollection.size() >= 1 && hashElementCollection.get(0) != null) { + ArrayCreationExpression value = hashElementCollection.get(0); + if(value != null) { + variables.addAll(PhpElementsUtil.getArrayValuesAsString(value)); + } + } + + Map defaults = new HashMap<>(); + if(hashElementCollection.size() >= 2 && hashElementCollection.get(1) != null) { + ArrayCreationExpression value = hashElementCollection.get(1); + if(value != null) { + defaults = PhpElementsUtil.getArrayKeyValueMap(value); + } + } + + Maprequirements = new HashMap<>(); + if(hashElementCollection.size() >= 3 && hashElementCollection.get(2) != null) { + ArrayCreationExpression value = hashElementCollection.get(2); + if(value != null) { + requirements = PhpElementsUtil.getArrayKeyValueMap(value); + } + } + + List> tokens = new ArrayList<>(); + if(hashElementCollection.size() >= 4 && hashElementCollection.get(3) != null) { + ArrayCreationExpression tokenArray = hashElementCollection.get(3); + if(tokenArray != null) { + for(ArrayHashElement tokenArrayConfig: tokenArray.getHashElements()) { + if(tokenArrayConfig.getValue() instanceof ArrayCreationExpression) { + Map arrayKeyValueMap = PhpElementsUtil.getArrayKeyValueMap((ArrayCreationExpression) tokenArrayConfig.getValue()); + tokens.add(arrayKeyValueMap.values()); + } + } + } + + } + + // hostTokens = 4 need them? + return new Route(routeName, variables, defaults, requirements, tokens); + } + + /** + * Used in Symfony < 4 where routes are wrapped into a class + */ @NotNull private static Route convertRouteConfig(@NotNull String routeName, @NotNull ArrayCreationExpression hashValue) { - List hashElementCollection = makeCollection(hashValue.getHashElements()); + List hashElementCollection = new ArrayList<>(); + hashValue.getHashElements().forEach(hashElementCollection::add); Set variables = new HashSet<>(); if(hashElementCollection.size() >= 1 && hashElementCollection.get(0).getValue() instanceof ArrayCreationExpression) { diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java index aebf16e30..73a2681e7 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java @@ -115,7 +115,7 @@ static public Map getArrayValuesAsMap(@NotNull ArrayCreation Map keys = new HashMap<>(); for (PsiElement child : arrayValues) { String stringValue = PhpElementsUtil.getStringValue(child.getFirstChild()); - if(stringValue != null && StringUtils.isNotBlank(stringValue)) { + if(StringUtils.isNotBlank(stringValue)) { keys.put(stringValue, child); } } @@ -123,6 +123,35 @@ static public Map getArrayValuesAsMap(@NotNull ArrayCreation return keys; } + /** + * array('foo' => FOO.class, 'foo1' => 'bar', 1 => 'foo') + */ + @NotNull + static public Map getArrayKeyValueMapWithValueAsPsiElement(@NotNull ArrayCreationExpression arrayCreationExpression) { + HashMap keys = new HashMap<>(); + + for(ArrayHashElement arrayHashElement: arrayCreationExpression.getHashElements()) { + PhpPsiElement child = arrayHashElement.getKey(); + if(child != null && ((child instanceof StringLiteralExpression) || PhpPatterns.psiElement(PhpElementTypes.NUMBER).accepts(child))) { + + String key; + if(child instanceof StringLiteralExpression) { + key = ((StringLiteralExpression) child).getContents(); + } else { + key = child.getText(); + } + + if(key == null || StringUtils.isBlank(key)) { + continue; + } + + keys.put(key, arrayHashElement.getValue()); + } + } + + return keys; + } + /** * array('foo' => 'bar', 'foo1' => 'bar', 1 => 'foo') */ diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/RouteHelperTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/RouteHelperTest.java index b50233f5e..22c0247d0 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/RouteHelperTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/RouteHelperTest.java @@ -264,6 +264,23 @@ public void testGetRoutesInsideUrlGeneratorFile() { assertNull(routes.get("_assetic_91dd2a8")); } + /** + * @see fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper#getRoutesInsideUrlGeneratorFile + */ + public void testGetRoutesInsideUrlGeneratorFileUrlGeneratorRoutes() { + Map routes = RouteHelper.getRoutesInsideUrlGeneratorFile(getProject(), myFixture.copyFileToProject("url_generating_routes.php")); + + Route previewError = routes.get("_preview_error"); + assertEquals("error_controller::preview", previewError.getController()); + assertContainsElements(previewError.getDefaults().keySet(), "_format", "_controller"); + assertContainsElements(previewError.getRequirements().keySet(), "code"); + assertContainsElements(previewError.getVariables(), "code", "_format"); + + Route profiler = routes.get("_profiler"); + assertEquals("web_profiler.controller.profiler::panelAction", profiler.getController()); + assertContainsElements(profiler.getVariables(), "token"); + } + /** * @see fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper#convertMethodToRouteShortcutControllerName */ diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/fixtures/url_generating_routes.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/fixtures/url_generating_routes.php new file mode 100644 index 000000000..c963f4563 --- /dev/null +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/fixtures/url_generating_routes.php @@ -0,0 +1,39 @@ + [ + ['code', '_format'], + ['_controller' => 'error_controller::preview', '_format' => 'html'], + ['code' => '\\d+'], + [ + ['variable', '.', '[^/]++', '_format', true], + ['variable', '/', '\\d+', 'code', true], + ['text', '/_error'] + ], + [], + [] + ], + '_wdt' => [['token'], ['_controller' => 'web_profiler.controller.profiler::toolbarAction'], [], [['variable', '/', '[^/]++', 'token', true], ['text', '/_wdt']], [], []], + '_profiler_home' => [[], ['_controller' => 'web_profiler.controller.profiler::homeAction'], [], [['text', '/_profiler/']], [], []], + '_profiler_search' => [[], ['_controller' => 'web_profiler.controller.profiler::searchAction'], [], [['text', '/_profiler/search']], [], []], + '_profiler_search_bar' => [[], ['_controller' => 'web_profiler.controller.profiler::searchBarAction'], [], [['text', '/_profiler/search_bar']], [], []], + '_profiler_phpinfo' => [[], ['_controller' => 'web_profiler.controller.profiler::phpinfoAction'], [], [['text', '/_profiler/phpinfo']], [], []], + '_profiler_search_results' => [['token'], ['_controller' => 'web_profiler.controller.profiler::searchResultsAction'], [], [['text', '/search/results'], ['variable', '/', '[^/]++', 'token', true], ['text', '/_profiler']], [], []], + '_profiler_open_file' => [[], ['_controller' => 'web_profiler.controller.profiler::openAction'], [], [['text', '/_profiler/open']], [], []], + '_profiler' => [ + ['token'], + ['_controller' => 'web_profiler.controller.profiler::panelAction'], + [], + [ + ['variable', '/', '[^/]++', 'token', true], + ['text', '/_profiler'] + ], + [], + [] + ], + '_profiler_router' => [['token'], ['_controller' => 'web_profiler.controller.router::panelAction'], [], [['text', '/router'], ['variable', '/', '[^/]++', 'token', true], ['text', '/_profiler']], [], []], + '_profiler_exception' => [['token'], ['_controller' => 'web_profiler.controller.exception_panel::body'], [], [['text', '/exception'], ['variable', '/', '[^/]++', 'token', true], ['text', '/_profiler']], [], []], + '_profiler_exception_css' => [['token'], ['_controller' => 'web_profiler.controller.exception_panel::stylesheet'], [], [['text', '/exception.css'], ['variable', '/', '[^/]++', 'token', true], ['text', '/_profiler']], [], []], +];