Skip to content

Commit 34ef21e

Browse files
author
Jérôme Deuchnord
committed
[Profiler] HttpClient: add button to copy a request to a cURL command
1 parent 01ea9a3 commit 34ef21e

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
Profile
9595
</th>
9696
{% endif %}
97+
{% if trace.curlCommand is not null %}
98+
<th>
99+
<button class="btn btn-sm hidden label" title="Copy as cURL command" data-clipboard-text="{{ trace.curlCommand }}">cURL</button>
100+
</th>
101+
{% endif %}
97102
</tr>
98103
</thead>
99104
<tbody>
@@ -110,7 +115,7 @@
110115
{{ trace.http_code }}
111116
</span>
112117
</th>
113-
<td>
118+
<td{% if trace.curlCommand %} colspan="2"{% endif %}>
114119
{{ profiler_dump(trace.info, maxDepth=1) }}
115120
</td>
116121
{% if profiler_token and profiler_link %}

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,29 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') {
3939
}
4040
4141
if (navigator.clipboard) {
42-
document.querySelectorAll('[data-clipboard-text]').forEach(function(element) {
43-
removeClass(element, 'hidden');
44-
element.addEventListener('click', function() {
45-
navigator.clipboard.writeText(element.getAttribute('data-clipboard-text'));
46-
})
42+
document.addEventListener('readystatechange', () => {
43+
if (document.readyState !== 'complete') {
44+
return;
45+
}
46+
47+
document.querySelectorAll('[data-clipboard-text]').forEach(function (element) {
48+
removeClass(element, 'hidden');
49+
element.addEventListener('click', function () {
50+
navigator.clipboard.writeText(element.getAttribute('data-clipboard-text'));
51+
52+
if (element.classList.contains("label")) {
53+
let oldContent = element.textContent;
54+
55+
element.textContent = "✅ Copied!";
56+
element.classList.add("status-success");
57+
58+
setTimeout(() => {
59+
element.textContent = oldContent;
60+
element.classList.remove("status-success");
61+
}, 7000);
62+
}
63+
});
64+
});
4765
});
4866
}
4967

src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,53 @@ private function collectOnClient(TraceableHttpClient $client): array
163163
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
164164
$traces[$i]['info'] = $this->cloneVar($info);
165165
$traces[$i]['options'] = $this->cloneVar($trace['options']);
166+
$traces[$i]['curlCommand'] = $this->getCurlCommand($trace);
166167
}
167168

168169
return [$errorCount, $traces];
169170
}
171+
172+
private function getCurlCommand(array $trace): ?string
173+
{
174+
$debug = explode("\n", ($trace['info']['debug']));
175+
$url = $trace['info']['url'];
176+
$command = ['curl', '--compressed'];
177+
178+
$dataArg = null;
179+
180+
if ($json = $trace['options']['json'] ?? null) {
181+
$dataArg = '--data '.escapeshellarg(json_encode($json, \JSON_PRETTY_PRINT));
182+
} elseif ($body = $trace['options']['body'] ?? null) {
183+
if (\is_string($body)) {
184+
$dataArg = '--data '.escapeshellarg($body);
185+
} elseif (\is_array($body)) {
186+
foreach ($body as $key => $value) {
187+
$dataArg = '--data '.escapeshellarg("$key=$value");
188+
}
189+
} else {
190+
return null;
191+
}
192+
}
193+
194+
foreach ($debug as $line) {
195+
$line = str_replace("\r", '', $line);
196+
if ('' === $line || preg_match('/^[*<]|(Host: )/', $line)) {
197+
continue;
198+
}
199+
200+
if (preg_match('/^> ([A-Z]+)/', $line, $match)) {
201+
$command[] = sprintf('--request %s', $match[1]);
202+
$command[] = sprintf('--url %s', escapeshellarg($url));
203+
continue;
204+
}
205+
206+
$command[] = '--header '.escapeshellarg($line);
207+
}
208+
209+
if (null !== $dataArg) {
210+
$command[] = $dataArg;
211+
}
212+
213+
return implode(" \\\n ", $command);
214+
}
170215
}

src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,49 @@ public function testItIsEmptyAfterReset()
166166
$this->assertEquals(0, $sut->getRequestCount());
167167
}
168168

169+
public function testItGeneratesCurlCommandsAsExpected()
170+
{
171+
$sut = new HttpClientDataCollector();
172+
$sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([
173+
[
174+
'method' => 'GET',
175+
'url' => 'https://symfony.com/releases.json',
176+
],
177+
]));
178+
$sut->collect(new Request(), new Response());
179+
$collectedData = $sut->getClients();
180+
self::assertCount(1, $collectedData['http_client']['traces']);
181+
$curlCommand = $collectedData['http_client']['traces'][0]['curlCommand'];
182+
self::assertEquals("curl \\
183+
--compressed \\
184+
--request GET \\
185+
--url 'https://symfony.com/releases.json' \\
186+
--header 'Accept: */*' \\
187+
--header 'Accept-Encoding: gzip' \\
188+
--header 'User-Agent: Symfony HttpClient/Native'", $curlCommand
189+
);
190+
}
191+
192+
public function testItDoesNotGeneratesCurlCommandsForUnsupportedBodyType()
193+
{
194+
$sut = new HttpClientDataCollector();
195+
$sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([
196+
[
197+
'method' => 'GET',
198+
'url' => 'https://symfony.com/releases.json',
199+
'options' => [
200+
'body' => static fn (int $size): string => '',
201+
],
202+
],
203+
]));
204+
$sut->collect(new Request(), new Response());
205+
$collectedData = $sut->getClients();
206+
self::assertCount(1, $collectedData['http_client']['traces']);
207+
$curlCommand = $collectedData['http_client']['traces'][0]['curlCommand'];
208+
self::assertNull($curlCommand
209+
);
210+
}
211+
169212
private function httpClientThatHasTracedRequests($tracedRequests): TraceableHttpClient
170213
{
171214
$httpClient = new TraceableHttpClient(new NativeHttpClient());

0 commit comments

Comments
 (0)