vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php line 39

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\HttpClient\DataCollector;
  11. use Symfony\Component\HttpClient\HttpClientTrait;
  12. use Symfony\Component\HttpClient\TraceableHttpClient;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\HttpKernel\DataCollector\DataCollector;
  16. use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
  17. use Symfony\Component\VarDumper\Caster\ImgStub;
  18. /**
  19.  * @author Jérémy Romey <jeremy@free-agent.fr>
  20.  */
  21. final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
  22. {
  23.     use HttpClientTrait;
  24.     /**
  25.      * @var TraceableHttpClient[]
  26.      */
  27.     private array $clients = [];
  28.     public function registerClient(string $nameTraceableHttpClient $client)
  29.     {
  30.         $this->clients[$name] = $client;
  31.     }
  32.     public function collect(Request $requestResponse $response\Throwable $exception null)
  33.     {
  34.         $this->lateCollect();
  35.     }
  36.     public function lateCollect()
  37.     {
  38.         $this->data['request_count'] = $this->data['request_count'] ?? 0;
  39.         $this->data['error_count'] = $this->data['error_count'] ?? 0;
  40.         $this->data += ['clients' => []];
  41.         foreach ($this->clients as $name => $client) {
  42.             [$errorCount$traces] = $this->collectOnClient($client);
  43.             $this->data['clients'] += [
  44.                 $name => [
  45.                     'traces' => [],
  46.                     'error_count' => 0,
  47.                 ],
  48.             ];
  49.             $this->data['clients'][$name]['traces'] = array_merge($this->data['clients'][$name]['traces'], $traces);
  50.             $this->data['request_count'] += \count($traces);
  51.             $this->data['error_count'] += $errorCount;
  52.             $this->data['clients'][$name]['error_count'] += $errorCount;
  53.             $client->reset();
  54.         }
  55.     }
  56.     public function getClients(): array
  57.     {
  58.         return $this->data['clients'] ?? [];
  59.     }
  60.     public function getRequestCount(): int
  61.     {
  62.         return $this->data['request_count'] ?? 0;
  63.     }
  64.     public function getErrorCount(): int
  65.     {
  66.         return $this->data['error_count'] ?? 0;
  67.     }
  68.     public function getName(): string
  69.     {
  70.         return 'http_client';
  71.     }
  72.     public function reset()
  73.     {
  74.         $this->data = [
  75.             'clients' => [],
  76.             'request_count' => 0,
  77.             'error_count' => 0,
  78.         ];
  79.     }
  80.     private function collectOnClient(TraceableHttpClient $client): array
  81.     {
  82.         $traces $client->getTracedRequests();
  83.         $errorCount 0;
  84.         $baseInfo = [
  85.             'response_headers' => 1,
  86.             'retry_count' => 1,
  87.             'redirect_count' => 1,
  88.             'redirect_url' => 1,
  89.             'user_data' => 1,
  90.             'error' => 1,
  91.             'url' => 1,
  92.         ];
  93.         foreach ($traces as $i => $trace) {
  94.             if (400 <= ($trace['info']['http_code'] ?? 0)) {
  95.                 ++$errorCount;
  96.             }
  97.             $info $trace['info'];
  98.             $traces[$i]['http_code'] = $info['http_code'] ?? 0;
  99.             unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
  100.             if (($info['http_method'] ?? null) === $trace['method']) {
  101.                 unset($info['http_method']);
  102.             }
  103.             if (($info['url'] ?? null) === $trace['url']) {
  104.                 unset($info['url']);
  105.             }
  106.             foreach ($info as $k => $v) {
  107.                 if (!$v || (is_numeric($v) && $v)) {
  108.                     unset($info[$k]);
  109.                 }
  110.             }
  111.             if (\is_string($content $trace['content'])) {
  112.                 $contentType 'application/octet-stream';
  113.                 foreach ($info['response_headers'] ?? [] as $h) {
  114.                     if (=== stripos($h'content-type: ')) {
  115.                         $contentType substr($h\strlen('content-type: '));
  116.                         break;
  117.                     }
  118.                 }
  119.                 if (str_starts_with($contentType'image/') && class_exists(ImgStub::class)) {
  120.                     $content = new ImgStub($content$contentType'');
  121.                 } else {
  122.                     $content = [$content];
  123.                 }
  124.                 $content = ['response_content' => $content];
  125.             } elseif (\is_array($content)) {
  126.                 $content = ['response_json' => $content];
  127.             } else {
  128.                 $content = [];
  129.             }
  130.             if (isset($info['retry_count'])) {
  131.                 $content['retries'] = $info['previous_info'];
  132.                 unset($info['previous_info']);
  133.             }
  134.             $debugInfo array_diff_key($info$baseInfo);
  135.             $info = ['info' => $debugInfo] + array_diff_key($info$debugInfo) + $content;
  136.             unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
  137.             $traces[$i]['info'] = $this->cloneVar($info);
  138.             $traces[$i]['options'] = $this->cloneVar($trace['options']);
  139.             $traces[$i]['curlCommand'] = $this->getCurlCommand($trace);
  140.         }
  141.         return [$errorCount$traces];
  142.     }
  143.     private function getCurlCommand(array $trace): ?string
  144.     {
  145.         if (!isset($trace['info']['debug'])) {
  146.             return null;
  147.         }
  148.         $url $trace['info']['original_url'] ?? $trace['info']['url'] ?? $trace['url'];
  149.         $command = ['curl''--compressed'];
  150.         if (isset($trace['options']['resolve'])) {
  151.             $port parse_url($url\PHP_URL_PORT) ?: (str_starts_with('http:'$url) ? 80 443);
  152.             foreach ($trace['options']['resolve'] as $host => $ip) {
  153.                 if (null !== $ip) {
  154.                     $command[] = '--resolve '.escapeshellarg("$host:$port:$ip");
  155.                 }
  156.             }
  157.         }
  158.         $dataArg = [];
  159.         if ($json $trace['options']['json'] ?? null) {
  160.             if (!$this->argMaxLengthIsSafe($payload self::jsonEncode($json))) {
  161.                 return null;
  162.             }
  163.             $dataArg[] = '--data '.escapeshellarg($payload);
  164.         } elseif ($body $trace['options']['body'] ?? null) {
  165.             if (\is_string($body)) {
  166.                 if (!$this->argMaxLengthIsSafe($body)) {
  167.                     return null;
  168.                 }
  169.                 try {
  170.                     $dataArg[] = '--data '.escapeshellarg($body);
  171.                 } catch (\ValueError) {
  172.                     return null;
  173.                 }
  174.             } elseif (\is_array($body)) {
  175.                 $body explode('&'self::normalizeBody($body));
  176.                 foreach ($body as $value) {
  177.                     if (!$this->argMaxLengthIsSafe($payload urldecode($value))) {
  178.                         return null;
  179.                     }
  180.                     $dataArg[] = '--data '.escapeshellarg($payload);
  181.                 }
  182.             } else {
  183.                 return null;
  184.             }
  185.         }
  186.         $dataArg = empty($dataArg) ? null implode(' '$dataArg);
  187.         foreach (explode("\n"$trace['info']['debug']) as $line) {
  188.             $line substr($line0, -1);
  189.             if (str_starts_with('< '$line)) {
  190.                 // End of the request, beginning of the response. Stop parsing.
  191.                 break;
  192.             }
  193.             if ('' === $line || preg_match('/^[*<]|(Host: )/'$line)) {
  194.                 continue;
  195.             }
  196.             if (preg_match('/^> ([A-Z]+)/'$line$match)) {
  197.                 $command[] = sprintf('--request %s'$match[1]);
  198.                 $command[] = sprintf('--url %s'escapeshellarg($url));
  199.                 continue;
  200.             }
  201.             $command[] = '--header '.escapeshellarg($line);
  202.         }
  203.         if (null !== $dataArg) {
  204.             $command[] = $dataArg;
  205.         }
  206.         return implode(" \\\n  "$command);
  207.     }
  208.     /**
  209.      * Let's be defensive : we authorize only size of 8kio on Windows for escapeshellarg() argument to avoid a fatal error.
  210.      *
  211.      * @see https://github.com/php/php-src/blob/9458f5f2c8a8e3d6c65cc181747a5a75654b7c6e/ext/standard/exec.c#L397
  212.      */
  213.     private function argMaxLengthIsSafe(string $payload): bool
  214.     {
  215.         return \strlen($payload) < ('\\' === \DIRECTORY_SEPARATOR 8100 256000);
  216.     }
  217. }