Skip to content

Commit 23f9bda

Browse files
committed
Refactor ServiceRouteContainer to cache routes resolved via services
1 parent ee7fcf0 commit 23f9bda

4 files changed

Lines changed: 93 additions & 68 deletions

File tree

src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1706,7 +1706,7 @@ public static List<Route> getRoutesOnControllerAction(@NotNull Method method) {
17061706
}
17071707

17081708
// search for services
1709-
routes.addAll(ServiceRouteContainer.build(project).getMethodMatches(method));
1709+
routes.addAll(ServiceRouteContainer.getMethodMatchesForRouteController(method));
17101710

17111711
return routes;
17121712
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/dic/ServiceRouteContainer.java

Lines changed: 54 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22

33
import com.intellij.openapi.project.Project;
44
import com.intellij.openapi.util.Key;
5+
import com.intellij.openapi.vfs.VirtualFileManager;
56
import com.intellij.psi.util.CachedValue;
67
import com.intellij.psi.util.CachedValueProvider;
78
import com.intellij.psi.util.CachedValuesManager;
8-
import com.intellij.psi.util.PsiModificationTracker;
9+
import com.jetbrains.php.lang.psi.stubs.indexes.PhpClassFqnIndex;
910
import com.jetbrains.php.lang.psi.elements.Method;
1011
import com.jetbrains.php.lang.psi.elements.PhpClass;
1112
import fr.adrienbrault.idea.symfony2plugin.routing.Route;
1213
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
1314
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
15+
import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches;
16+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.PhpAttributeIndex;
17+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.RoutesStubIndex;
18+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ServicesDefinitionStubIndex;
19+
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyVarDirectoryWatcher;
20+
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyVarDirectoryWatcherKt;
1421
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
1522
import org.apache.commons.lang3.StringUtils;
1623
import org.jetbrains.annotations.NotNull;
@@ -22,78 +29,43 @@
2229
*/
2330
public class ServiceRouteContainer {
2431

25-
private static final Key<CachedValue<ServiceRouteContainer>> SERVICE_ROUTE_CONTAINER_CACHE = new Key<>("SYMFONY_SERVICE_ROUTE_CONTAINER_CACHE");
26-
27-
private final Collection<Route> routes;
28-
// Lazily built index: method name -> matching route entries. Null until first use.
29-
private Map<String, List<RouteEntry>> routesByMethodName;
30-
private ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector;
31-
32-
private final Map<String, PhpClass> serviceCache = new HashMap<>();
32+
private static final Key<CachedValue<ServiceRouteData>> SERVICE_ROUTE_DATA_CACHE = new Key<>("SYMFONY_SERVICE_ROUTE_DATA_CACHE");
3333

3434
// Holds the pre-split controller parts alongside the Route to avoid re-splitting on every lookup.
3535
private record RouteEntry(String serviceId, String methodName, Route route) {}
3636

37-
private ServiceRouteContainer(@NotNull Collection<Route> routes) {
38-
this.routes = routes;
39-
}
37+
private record ServiceRouteData(@NotNull Map<String, List<RouteEntry>> routesByMethodName, @NotNull Set<String> serviceNames) {}
4038

4139
/**
42-
* Builds the method-name index on the first call and caches it for subsequent calls.
43-
* Splitting is done here once with indexOf instead of split() to avoid repeated array allocations.
40+
* Returns service ids referenced by service-style route controllers.
4441
*/
45-
private Map<String, List<RouteEntry>> getRoutesByMethodName() {
46-
if (routesByMethodName != null) {
47-
return routesByMethodName;
48-
}
49-
50-
Map<String, List<RouteEntry>> index = new HashMap<>();
51-
for (Route route : routes) {
52-
String controller = route.getController();
53-
if (controller == null) continue;
54-
// controller format: "service_id:methodName" or "service_id::methodName"
55-
String normalizedController = controller.replace("::", ":");
56-
int colon = normalizedController.indexOf(':');
57-
if (colon > 0 && colon < normalizedController.length() - 1) {
58-
String serviceId = normalizedController.substring(0, colon);
59-
String methodName = normalizedController.substring(colon + 1);
60-
index.computeIfAbsent(methodName, k -> new ArrayList<>()).add(new RouteEntry(serviceId, methodName, route));
61-
}
62-
}
63-
64-
return routesByMethodName = index;
65-
}
66-
67-
public Set<String> getServiceNames() {
68-
Set<String> services = new HashSet<>();
69-
for (List<RouteEntry> entries : getRoutesByMethodName().values()) {
70-
for (RouteEntry entry : entries) {
71-
services.add(entry.serviceId());
72-
}
73-
}
74-
return services;
42+
@NotNull
43+
public static Set<String> getServiceNames(@NotNull Project project) {
44+
return getServiceRouteData(project).serviceNames();
7545
}
7646

7747
@NotNull
78-
public Collection<Route> getMethodMatches(@NotNull Method method) {
48+
public static Collection<Route> getMethodMatchesForRouteController(@NotNull Method method) {
7949
PhpClass originClass = method.getContainingClass();
8050
if(originClass == null) {
8151
return Collections.emptyList();
8252
}
8353

8454
// Fast path: no routes registered for this method name at all
85-
List<RouteEntry> candidates = getRoutesByMethodName().get(method.getName());
55+
List<RouteEntry> candidates = getServiceRouteData(method.getProject()).routesByMethodName().get(method.getName());
8656
if (candidates == null) {
8757
return Collections.emptyList();
8858
}
8959

9060
String classFqn = StringUtils.stripStart(originClass.getFQN(), "\\");
9161

9262
Collection<Route> routes = new ArrayList<>();
63+
Map<String, PhpClass> serviceCache = new HashMap<>();
64+
ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector = new ContainerCollectionResolver.LazyServiceCollector(method.getProject());
9365
for (RouteEntry entry : candidates) {
9466
// cache PhpClass resolve
9567
if(!serviceCache.containsKey(entry.serviceId())) {
96-
serviceCache.put(entry.serviceId(), ServiceUtil.getResolvedClassDefinition(method.getProject(), entry.serviceId(), getLazyServiceCollector(method.getProject())));
68+
serviceCache.put(entry.serviceId(), ServiceUtil.getResolvedClassDefinition(method.getProject(), entry.serviceId(), lazyServiceCollector));
9769
}
9870

9971
PhpClass phpClass = serviceCache.get(entry.serviceId());
@@ -107,33 +79,39 @@ public Collection<Route> getMethodMatches(@NotNull Method method) {
10779
return routes;
10880
}
10981

110-
private ContainerCollectionResolver.LazyServiceCollector getLazyServiceCollector(Project project) {
111-
return this.lazyServiceCollector == null ? this.lazyServiceCollector = new ContainerCollectionResolver.LazyServiceCollector(project) : this.lazyServiceCollector;
112-
}
113-
11482
@NotNull
115-
public static ServiceRouteContainer build(@NotNull Project project) {
83+
private static ServiceRouteData getServiceRouteData(@NotNull Project project) {
11684
return CachedValuesManager.getManager(project).getCachedValue(
11785
project,
118-
SERVICE_ROUTE_CONTAINER_CACHE,
119-
() -> {
120-
ServiceRouteContainer container = buildUncached(project, RouteHelper.getAllRoutes(project));
121-
return CachedValueProvider.Result.create(
122-
container,
123-
PsiModificationTracker.MODIFICATION_COUNT
124-
);
125-
},
86+
SERVICE_ROUTE_DATA_CACHE,
87+
() -> CachedValueProvider.Result.create(
88+
buildUncached(project, RouteHelper.getAllRoutes(project)),
89+
getCacheDependencies(project)
90+
),
12691
false
12792
);
12893
}
12994

95+
private static Object @NotNull [] getCacheDependencies(@NotNull Project project) {
96+
return new Object[] {
97+
FileIndexCaches.getModificationTrackerForIndexId(project, RoutesStubIndex.KEY),
98+
SymfonyVarDirectoryWatcherKt.getSymfonyVarDirectoryWatcher(project).getModificationTracker(SymfonyVarDirectoryWatcher.Scope.ROUTES),
99+
FileIndexCaches.getModificationTrackerForIndexId(project, ServicesDefinitionStubIndex.KEY),
100+
FileIndexCaches.getModificationTrackerForIndexId(project, PhpAttributeIndex.KEY),
101+
FileIndexCaches.getModificationTrackerForIndexId(project, PhpClassFqnIndex.KEY),
102+
SymfonyVarDirectoryWatcherKt.getSymfonyVarDirectoryWatcher(project).getModificationTracker(SymfonyVarDirectoryWatcher.Scope.CONTAINER),
103+
VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS
104+
};
105+
}
106+
130107
/**
131-
* Build container which stores all service routes
108+
* Build route lookup data for service-style route controllers.
132109
*
133110
* @param routes Unfiltered routes
134111
*/
135-
private static ServiceRouteContainer buildUncached(@NotNull Project project, @NotNull Map<String, Route> routes) {
136-
Collection<Route> serviceRoutes = new ArrayList<>();
112+
private static ServiceRouteData buildUncached(@NotNull Project project, @NotNull Map<String, Route> routes) {
113+
Map<String, List<RouteEntry>> routesByMethodName = new HashMap<>();
114+
Set<String> serviceNames = new HashSet<>();
137115

138116
ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector = null;
139117

@@ -150,11 +128,21 @@ private static ServiceRouteContainer buildUncached(@NotNull Project project, @No
150128
}
151129

152130
if(split.length > 1 && ContainerCollectionResolver.hasServiceName(lazyServiceCollector, split[0])) {
153-
serviceRoutes.add(route);
131+
String serviceId = split[0];
132+
String methodName = split[1];
133+
routesByMethodName.computeIfAbsent(methodName, key -> new ArrayList<>()).add(new RouteEntry(serviceId, methodName, route));
134+
serviceNames.add(serviceId);
154135
}
155136
}
156137

157-
return new ServiceRouteContainer(serviceRoutes);
158-
}
138+
Map<String, List<RouteEntry>> unmodifiableRoutesByMethodName = new HashMap<>();
139+
for (Map.Entry<String, List<RouteEntry>> entry : routesByMethodName.entrySet()) {
140+
unmodifiableRoutesByMethodName.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
141+
}
159142

143+
return new ServiceRouteData(
144+
Collections.unmodifiableMap(unmodifiableRoutesByMethodName),
145+
Collections.unmodifiableSet(serviceNames)
146+
);
147+
}
160148
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/util/controller/ControllerIndex.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private Collection<ControllerAction> getServiceActionMethods(@NotNull Project pr
146146
ContainerCollectionResolver.LazyServiceCollector collector = new ContainerCollectionResolver.LazyServiceCollector(project);
147147

148148
Collection<ControllerAction> actions = new ArrayList<>();
149-
for (String serviceName : ServiceRouteContainer.build(project).getServiceNames()) {
149+
for (String serviceName : ServiceRouteContainer.getServiceNames(project)) {
150150
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, serviceName, collector);
151151
if(phpClass == null) {
152152
continue;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.routing.dic;
2+
3+
import com.jetbrains.php.lang.psi.elements.Method;
4+
import fr.adrienbrault.idea.symfony2plugin.routing.Route;
5+
import fr.adrienbrault.idea.symfony2plugin.routing.dic.ServiceRouteContainer;
6+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
7+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
8+
9+
import java.util.Collection;
10+
11+
public class ServiceRouteContainerTest extends SymfonyLightCodeInsightFixtureTestCase {
12+
public String getTestDataPath() {
13+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/fixtures";
14+
}
15+
16+
public void testGetServiceNamesForServiceControllerRoutes() {
17+
configureServiceControllerRouteFixtures();
18+
19+
assertContainsElements(ServiceRouteContainer.getServiceNames(getProject()), "foo.bar_controller");
20+
}
21+
22+
public void testGetMethodMatchesForServiceControllerClassMethod() {
23+
configureServiceControllerRouteFixtures();
24+
25+
Method method = PhpElementsUtil.getClassMethod(getProject(), "Service\\Controller\\FooController", "indexAction");
26+
assertNotNull(method);
27+
28+
Collection<Route> routes = ServiceRouteContainer.getMethodMatchesForRouteController(method);
29+
assertNotNull(routes.stream().filter(route -> "xml_route_as_service".equals(route.getName())).findFirst().orElse(null));
30+
}
31+
32+
private void configureServiceControllerRouteFixtures() {
33+
myFixture.copyFileToProject("GetRoutesOnControllerAction.php");
34+
myFixture.copyFileToProject("GetRoutesOnControllerAction.routing.xml");
35+
myFixture.copyFileToProject("GetRoutesOnControllerAction.services.xml");
36+
}
37+
}

0 commit comments

Comments
 (0)