Skip to content

Commit 0dbe5db

Browse files
committed
feat(profiler): add cURL copy/paste to request tab
1 parent a4a0463 commit 0dbe5db

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support for the `QUERY` HTTP method in the profiler
88
* Add support for Server-Sent Events / `EventSource` requests in the debug toolbar
99
* Add support for displaying the application runner class
10+
* Add cURL copy paste button in the Request/Response tab
1011

1112
7.3
1213
---

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,25 @@
232232
<p>No content</p>
233233
</div>
234234
{% endif %}
235+
236+
<h3>Retry command</h3>
237+
{% if collector.curlCommand != "" %}
238+
<div class="sf-tabs">
239+
<div class="tab">
240+
<h3 class="tab-title">cURL</h3>
241+
<div class="tab-content">
242+
<div class="card">
243+
<button class="btn btn-sm hidden" title="Copy as cURL" data-clipboard-text="{{ collector.curlCommand }}">Copy as cURL</button>
244+
<pre class="break-long-words">{{ collector.curlCommand }}</pre>
245+
</div>
246+
</div>
247+
</div>
248+
</div>
249+
{% else %}
250+
<div class="empty">
251+
<p>No content</p>
252+
</div>
253+
{% endif %}
235254
</div>
236255
</div>
237256

src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\HttpKernel\Event\ControllerEvent;
2323
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2424
use Symfony\Component\HttpKernel\KernelEvents;
25+
use Symfony\Component\Process\Process;
2526
use Symfony\Component\VarDumper\Cloner\Data;
2627

2728
/**
@@ -112,6 +113,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep
112113
'controller' => 'n/a',
113114
'locale' => $request->getLocale(),
114115
'dotenv_vars' => $dotenvVars,
116+
'curlCommand' => $this->getCurlCommand(),
115117
];
116118

117119
if (isset($this->data['request_headers']['php-auth-pw'])) {
@@ -495,4 +497,76 @@ private function parseController(array|object|string|null $controller): array|st
495497

496498
return \is_string($controller) ? $controller : 'n/a';
497499
}
500+
501+
public function getCurlCommand(): ?string
502+
{
503+
if (!isset($this->data['method']) || !isset($this->data['path_info'])) {
504+
return null;
505+
}
506+
507+
$command = ['curl', '--compressed'];
508+
509+
// Build the full URL
510+
$scheme = $this->data['request_server']['HTTPS'] ?? false ? 'https' : 'http';
511+
$host = $this->data['request_server']['HTTP_HOST'] ?? $this->data['request_server']['SERVER_NAME'] ?? 'localhost';
512+
$url = $scheme . '://' . $host . $this->data['path_info'];
513+
514+
if (!empty($this->data['request_query'])) {
515+
$url .= '?' . http_build_query($this->data['request_query']->getValue());
516+
}
517+
518+
// Add HTTP method
519+
$method = $this->data['method'];
520+
if ($method !== 'GET') {
521+
$command[] = sprintf('--request %s', $method);
522+
}
523+
524+
$command[] = sprintf('--url %s', escapeshellarg($url));
525+
526+
// Add headers
527+
foreach ($this->data['request_headers']->getValue() as $name => $value) {
528+
if (is_array($value)) {
529+
$value = implode(', ', $value);
530+
}
531+
532+
// Skip certain headers
533+
if (in_array(strtolower($name), ['host', 'cookie'])) {
534+
continue;
535+
}
536+
537+
$command[] = '--header ' . escapeshellarg(ucwords($name, '-') . ': ' . $value);
538+
}
539+
540+
// Add cookies
541+
if (!empty($this->data['request_cookies']->getValue())) {
542+
$cookies = [];
543+
foreach ($this->data['request_cookies']->getValue() as $name => $value) {
544+
$cookies[] = $name . '=' . $value;
545+
}
546+
$command[] = '--header ' . escapeshellarg('Cookie: ' . implode('; ', $cookies));
547+
}
548+
549+
// Add body data
550+
$content = $this->data['content'];
551+
if (!empty($content) && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
552+
$command[] = '--data-raw ' . $this->escapePayload($content);
553+
}
554+
555+
return implode(" \\\n ", $command);
556+
}
557+
558+
private function escapePayload(string $payload): string
559+
{
560+
static $useProcess;
561+
562+
if ($useProcess ??= \function_exists('proc_open') && class_exists(\Symfony\Component\Process\Process::class)) {
563+
return substr((new Process(['', $payload]))->getCommandLine(), 3);
564+
}
565+
566+
if ('\\' === \DIRECTORY_SEPARATOR) {
567+
return '"' . str_replace('"', '""', $payload) . '"';
568+
}
569+
570+
return "'" . str_replace("'", "'\\''", $payload) . "'";
571+
}
498572
}

0 commit comments

Comments
 (0)