Skip to content

Commit 386e03c

Browse files
committed
Introduce LocalTwigComponentDataCollector for Symfony UX TwigComponent profiling integration
1 parent 7fe9f5d commit 386e03c

8 files changed

Lines changed: 564 additions & 4 deletions

File tree

src/main/java/fr/adrienbrault/idea/symfony2plugin/profiler/LocalProfilerIndex.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.intellij.util.containers.ContainerUtil;
44
import fr.adrienbrault.idea.symfony2plugin.profiler.collector.LocalDefaultDataCollector;
55
import fr.adrienbrault.idea.symfony2plugin.profiler.collector.LocalMailCollector;
6+
import fr.adrienbrault.idea.symfony2plugin.profiler.collector.LocalTwigComponentDataCollector;
67
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.LocalProfilerRequest;
78
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.ProfilerRequestInterface;
89
import fr.adrienbrault.idea.symfony2plugin.profiler.reader.ReverseFileLineReader;
@@ -135,7 +136,8 @@ public ProfilerRequestInterface call() {
135136
return new LocalProfilerRequest(
136137
split,
137138
new LocalDefaultDataCollector(content),
138-
new LocalMailCollector(content)
139+
new LocalMailCollector(content),
140+
new LocalTwigComponentDataCollector(content)
139141
);
140142
}
141143
}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
package fr.adrienbrault.idea.symfony2plugin.profiler.collector;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.ProfilerTwigComponent;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.util.ArrayList;
8+
import java.util.Collection;
9+
import java.util.Collections;
10+
import java.util.regex.Matcher;
11+
import java.util.regex.Pattern;
12+
13+
public class LocalTwigComponentDataCollector implements TwigComponentDataCollectorInterface {
14+
private static final String TWIG_COMPONENT_COLLECTOR_KEY = "s:14:\"twig_component\"";
15+
private static final Pattern SERIALIZED_ARRAY_PREFIX = Pattern.compile("a:\\d+:\\{");
16+
private static final Pattern SERIALIZED_STRING_PREFIX = Pattern.compile("s:(\\d+):\"");
17+
18+
@Nullable
19+
private final String contents;
20+
21+
public LocalTwigComponentDataCollector(@Nullable String contents) {
22+
this.contents = contents;
23+
}
24+
25+
@Override
26+
public @NotNull Collection<ProfilerTwigComponent> getComponents() {
27+
String collectorContent = getTwigComponentCollectorContent();
28+
if (collectorContent == null) {
29+
return Collections.emptyList();
30+
}
31+
32+
ComponentsContent componentsContent = findComponentsContent(collectorContent);
33+
if (componentsContent == null) {
34+
return Collections.emptyList();
35+
}
36+
37+
Collection<ProfilerTwigComponent> components = new ArrayList<>();
38+
for (String componentContent : getDirectValues(componentsContent.componentsContent())) {
39+
componentContent = resolveVarDumperReference(componentContent, componentsContent.dataContent());
40+
ComponentFields componentFields = parseComponentFields(componentContent);
41+
if (!componentFields.isComponent()) {
42+
continue;
43+
}
44+
45+
components.add(new ProfilerTwigComponent(
46+
componentFields.name,
47+
componentFields.className,
48+
componentFields.template,
49+
componentFields.renderCount != null ? componentFields.renderCount : 0
50+
));
51+
}
52+
53+
return components;
54+
}
55+
56+
@Nullable
57+
private static ComponentsContent findComponentsContent(@NotNull String collectorContent) {
58+
String dataObjectContent = findDirectValueByKey(collectorContent, "data");
59+
if (dataObjectContent == null) {
60+
return null;
61+
}
62+
63+
String dataContent = findDirectValueByKey(dataObjectContent, "data");
64+
if (dataContent == null) {
65+
return null;
66+
}
67+
68+
for (String dataValue : getDirectValues(dataContent)) {
69+
String componentsReference = findDirectValueByKey(dataValue, "components");
70+
if (componentsReference == null) {
71+
continue;
72+
}
73+
74+
return new ComponentsContent(resolveVarDumperReference(componentsReference, dataContent), dataContent);
75+
}
76+
77+
return null;
78+
}
79+
80+
@Nullable
81+
private String getTwigComponentCollectorContent() {
82+
if (contents == null || !contents.contains(TWIG_COMPONENT_COLLECTOR_KEY)) {
83+
return null;
84+
}
85+
86+
int collectorIndex = contents.indexOf(TWIG_COMPONENT_COLLECTOR_KEY);
87+
int valueStart = contents.indexOf(';', collectorIndex + TWIG_COMPONENT_COLLECTOR_KEY.length());
88+
if (valueStart < 0) {
89+
return contents.substring(collectorIndex);
90+
}
91+
92+
int valueEnd = findSerializedValueEnd(contents, valueStart + 1);
93+
return valueEnd > valueStart ? contents.substring(valueStart + 1, valueEnd) : contents.substring(collectorIndex);
94+
}
95+
96+
private static int findSerializedValueEnd(@NotNull String content, int start) {
97+
int depth = 0;
98+
boolean hasCompoundValue = false;
99+
100+
for (int i = start; i < content.length(); i++) {
101+
int stringEnd = findSerializedStringEnd(content, i);
102+
if (stringEnd > i) {
103+
i = stringEnd - 1;
104+
continue;
105+
}
106+
107+
char c = content.charAt(i);
108+
if (c == '{') {
109+
depth++;
110+
hasCompoundValue = true;
111+
} else if (c == '}') {
112+
depth--;
113+
if (hasCompoundValue && depth == 0) {
114+
return i + 1;
115+
}
116+
} else if (!hasCompoundValue && c == ';') {
117+
return i + 1;
118+
}
119+
}
120+
121+
return -1;
122+
}
123+
124+
@Nullable
125+
private static SerializedValue readSerializedValue(@NotNull String content, int start) {
126+
if (content.startsWith("N;", start)) {
127+
return new SerializedValue(null, null, start + 2);
128+
}
129+
130+
SerializedValue stringValue = readSerializedString(content, start);
131+
if (stringValue != null) {
132+
return stringValue;
133+
}
134+
135+
if (content.startsWith("i:", start)) {
136+
int end = content.indexOf(';', start + 2);
137+
if (end > start) {
138+
return new SerializedValue(null, parseInteger(content.substring(start + 2, end)), end + 1);
139+
}
140+
}
141+
142+
int valueEnd = findSerializedValueEnd(content, start);
143+
return valueEnd > start ? new SerializedValue(null, null, valueEnd) : null;
144+
}
145+
146+
@Nullable
147+
private static SerializedValue readSerializedString(@NotNull String content, int start) {
148+
Matcher matcher = SERIALIZED_STRING_PREFIX.matcher(content);
149+
matcher.region(start, content.length());
150+
if (!matcher.lookingAt()) {
151+
return null;
152+
}
153+
154+
int length;
155+
try {
156+
length = Integer.parseInt(matcher.group(1));
157+
} catch (NumberFormatException ignored) {
158+
return null;
159+
}
160+
161+
int stringEnd = matcher.end() + length;
162+
if (stringEnd + 2 <= content.length() && content.startsWith("\";", stringEnd)) {
163+
return new SerializedValue(content.substring(matcher.end(), stringEnd), null, stringEnd + 2);
164+
}
165+
166+
int fallbackStringEnd = content.indexOf("\";", matcher.end());
167+
if (fallbackStringEnd > matcher.end()) {
168+
return new SerializedValue(content.substring(matcher.end(), fallbackStringEnd), null, fallbackStringEnd + 2);
169+
}
170+
171+
return null;
172+
}
173+
174+
private static int findSerializedStringEnd(@NotNull String content, int start) {
175+
SerializedValue value = readSerializedString(content, start);
176+
return value != null ? value.end : -1;
177+
}
178+
179+
@Nullable
180+
private static String findDirectValueByKey(@NotNull String serializedContent, @NotNull String expectedKey) {
181+
for (SerializedPair pair : getDirectPairs(serializedContent)) {
182+
if (pair.key.stringValue != null && isPropertyName(pair.key.stringValue, expectedKey)) {
183+
return pair.rawValue;
184+
}
185+
}
186+
187+
return null;
188+
}
189+
190+
@Nullable
191+
private static String findDirectValueByIntegerKey(@NotNull String serializedContent, int expectedKey) {
192+
for (SerializedPair pair : getDirectPairs(serializedContent)) {
193+
if (pair.key.integerValue != null && pair.key.integerValue == expectedKey) {
194+
return pair.rawValue;
195+
}
196+
}
197+
198+
return null;
199+
}
200+
201+
@NotNull
202+
private static String resolveVarDumperReference(@NotNull String serializedContent, @NotNull String dataContent) {
203+
String referencedIndex = findDirectValueByIntegerKey(serializedContent, 1);
204+
if (referencedIndex == null) {
205+
return serializedContent;
206+
}
207+
208+
SerializedValue referenceValue = readSerializedValue(referencedIndex, 0);
209+
if (referenceValue == null || referenceValue.integerValue == null) {
210+
return serializedContent;
211+
}
212+
213+
String resolvedContent = findDirectValueByIntegerKey(dataContent, referenceValue.integerValue);
214+
return resolvedContent != null ? resolvedContent : serializedContent;
215+
}
216+
217+
@NotNull
218+
private static Collection<String> getDirectValues(@NotNull String serializedContent) {
219+
Collection<String> values = new ArrayList<>();
220+
for (SerializedPair pair : getDirectPairs(serializedContent)) {
221+
values.add(pair.rawValue);
222+
}
223+
224+
return values;
225+
}
226+
227+
@NotNull
228+
private static Collection<SerializedPair> getDirectPairs(@NotNull String serializedContent) {
229+
Collection<SerializedPair> pairs = new ArrayList<>();
230+
231+
Matcher arrayMatcher = SERIALIZED_ARRAY_PREFIX.matcher(serializedContent);
232+
if (!arrayMatcher.lookingAt() && !serializedContent.startsWith("O:")) {
233+
return pairs;
234+
}
235+
236+
int bodyStart = serializedContent.indexOf('{');
237+
if (bodyStart < 0) {
238+
return pairs;
239+
}
240+
241+
int offset = bodyStart + 1;
242+
while (offset < serializedContent.length() && serializedContent.charAt(offset) != '}') {
243+
SerializedValue key = readSerializedValue(serializedContent, offset);
244+
if (key == null) {
245+
break;
246+
}
247+
248+
offset = key.end;
249+
250+
int valueStart = offset;
251+
SerializedValue value = readSerializedValue(serializedContent, valueStart);
252+
if (value == null) {
253+
break;
254+
}
255+
256+
offset = value.end;
257+
pairs.add(new SerializedPair(key, value, serializedContent.substring(valueStart, value.end)));
258+
}
259+
260+
return pairs;
261+
}
262+
263+
private static boolean isPropertyName(@NotNull String key, @NotNull String expectedKey) {
264+
int nullByte = key.lastIndexOf('\0');
265+
String normalizedKey = nullByte >= 0 ? key.substring(nullByte + 1) : key;
266+
return expectedKey.equals(normalizedKey);
267+
}
268+
269+
@NotNull
270+
private static ComponentFields parseComponentFields(@NotNull String componentArrayContent) {
271+
ComponentFields fields = new ComponentFields();
272+
273+
for (SerializedPair pair : getDirectPairs(componentArrayContent)) {
274+
if (pair.key.stringValue == null) {
275+
continue;
276+
}
277+
278+
switch (pair.key.stringValue) {
279+
case "name" -> fields.name = pair.value.stringValue;
280+
case "class" -> fields.className = normalizePhpClassName(pair.value.stringValue);
281+
case "template" -> fields.template = pair.value.stringValue;
282+
case "render_count" -> fields.renderCount = pair.value.integerValue;
283+
}
284+
}
285+
286+
return fields;
287+
}
288+
289+
@Nullable
290+
private static String normalizePhpClassName(@Nullable String className) {
291+
if (className == null || className.isBlank()) {
292+
return null;
293+
}
294+
295+
return className.startsWith("\\") ? className : "\\" + className;
296+
}
297+
298+
@Nullable
299+
private static Integer parseInteger(@Nullable String value) {
300+
if (value == null) {
301+
return null;
302+
}
303+
304+
try {
305+
return Integer.parseInt(value);
306+
} catch (NumberFormatException ignored) {
307+
return null;
308+
}
309+
}
310+
311+
private static class ComponentFields {
312+
private String name;
313+
private String className;
314+
private String template;
315+
private Integer renderCount;
316+
317+
private boolean isComponent() {
318+
return name != null && (className != null || template != null || renderCount != null);
319+
}
320+
}
321+
322+
private record SerializedValue(@Nullable String stringValue, @Nullable Integer integerValue, int end) {
323+
}
324+
325+
private record SerializedPair(@NotNull SerializedValue key, @NotNull SerializedValue value, @NotNull String rawValue) {
326+
}
327+
328+
private record ComponentsContent(@NotNull String componentsContent, @NotNull String dataContent) {
329+
}
330+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package fr.adrienbrault.idea.symfony2plugin.profiler.collector;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.ProfilerTwigComponent;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
import java.util.Collection;
7+
8+
public interface TwigComponentDataCollectorInterface {
9+
@NotNull
10+
Collection<ProfilerTwigComponent> getComponents();
11+
}

0 commit comments

Comments
 (0)