Skip to content

Commit f2d88d5

Browse files
Jérôme Deuchnordfabpot
authored andcommitted
[HttpClient][WebProfilerBundle] Add button to copy a request as a cURL command
1 parent 23a7f02 commit f2d88d5

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

DataCollector/HttpClientDataCollector.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,61 @@ 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['url'];
176+
$command = ['curl', '--compressed'];
177+
178+
$dataArg = [];
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+
$dataArg = empty($dataArg) ? null : implode(' ', $dataArg);
195+
196+
foreach ($debug as $line) {
197+
$line = substr($line, 0, -1);
198+
199+
if (str_starts_with('< ', $line)) {
200+
// End of the request, beginning of the response. Stop parsing.
201+
break;
202+
}
203+
204+
if ('' === $line || preg_match('/^[*<]|(Host: )/', $line)) {
205+
continue;
206+
}
207+
208+
if (preg_match('/^> ([A-Z]+)/', $line, $match)) {
209+
$command[] = sprintf('--request %s', $match[1]);
210+
$command[] = sprintf('--url %s', escapeshellarg($url));
211+
continue;
212+
}
213+
214+
$command[] = '--header '.escapeshellarg($line);
215+
}
216+
217+
if (null !== $dataArg) {
218+
$command[] = $dataArg;
219+
}
220+
221+
return implode(" \\\n ", $command);
222+
}
170223
}

Tests/DataCollector/HttpClientDataCollectorTest.php

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

169+
/**
170+
* @requires extension openssl
171+
*/
172+
public function testItGeneratesCurlCommandsAsExpected()
173+
{
174+
$sut = new HttpClientDataCollector();
175+
$sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([
176+
[
177+
'method' => 'GET',
178+
'url' => 'https://symfony.com/releases.json',
179+
],
180+
]));
181+
$sut->collect(new Request(), new Response());
182+
$collectedData = $sut->getClients();
183+
self::assertCount(1, $collectedData['http_client']['traces']);
184+
$curlCommand = $collectedData['http_client']['traces'][0]['curlCommand'];
185+
self::assertEquals("curl \\
186+
--compressed \\
187+
--request GET \\
188+
--url 'https://symfony.com/releases.json' \\
189+
--header 'Accept: */*' \\
190+
--header 'Accept-Encoding: gzip' \\
191+
--header 'User-Agent: Symfony HttpClient/Native'", $curlCommand
192+
);
193+
}
194+
195+
/**
196+
* @requires extension openssl
197+
*/
198+
public function testItDoesNotFollowRedirectionsWhenGeneratingCurlCommands()
199+
{
200+
$sut = new HttpClientDataCollector();
201+
$sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([
202+
[
203+
'method' => 'GET',
204+
'url' => 'http://symfony.com/releases.json',
205+
],
206+
]));
207+
$sut->collect(new Request(), new Response());
208+
$collectedData = $sut->getClients();
209+
self::assertCount(1, $collectedData['http_client']['traces']);
210+
$curlCommand = $collectedData['http_client']['traces'][0]['curlCommand'];
211+
self::assertEquals("curl \\
212+
--compressed \\
213+
--request GET \\
214+
--url 'http://symfony.com/releases.json' \\
215+
--header 'Accept: */*' \\
216+
--header 'Accept-Encoding: gzip' \\
217+
--header 'User-Agent: Symfony HttpClient/Native'", $curlCommand
218+
);
219+
}
220+
221+
/**
222+
* @requires extension openssl
223+
*/
224+
public function testItDoesNotGeneratesCurlCommandsForUnsupportedBodyType()
225+
{
226+
$sut = new HttpClientDataCollector();
227+
$sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([
228+
[
229+
'method' => 'GET',
230+
'url' => 'https://symfony.com/releases.json',
231+
'options' => [
232+
'body' => static fn (int $size): string => '',
233+
],
234+
],
235+
]));
236+
$sut->collect(new Request(), new Response());
237+
$collectedData = $sut->getClients();
238+
self::assertCount(1, $collectedData['http_client']['traces']);
239+
$curlCommand = $collectedData['http_client']['traces'][0]['curlCommand'];
240+
self::assertNull($curlCommand
241+
);
242+
}
243+
169244
private function httpClientThatHasTracedRequests($tracedRequests): TraceableHttpClient
170245
{
171246
$httpClient = new TraceableHttpClient(new NativeHttpClient());

0 commit comments

Comments
 (0)