diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index fb9228b106..30874c940d 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\VarDumper\Caster\ImgStub; /** * @author Jérémy Romey @@ -128,8 +129,29 @@ final class HttpClientDataCollector extends DataCollector implements LateDataCol } } + if (\is_string($content = $trace['content'])) { + $contentType = 'application/octet-stream'; + + foreach ($info['response_headers'] ?? [] as $h) { + if (0 === stripos($h, 'content-type: ')) { + $contentType = substr($h, \strlen('content-type: ')); + break; + } + } + + if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) { + $content = new ImgStub($content, $contentType, ''); + } else { + $content = [$content]; + } + + $k = 'response_content'; + } else { + $k = 'response_json'; + } + $debugInfo = array_diff_key($info, $baseInfo); - $info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo]; + $info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + [$k => $content]; unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient $traces[$i]['info'] = $this->cloneVar($info); $traces[$i]['options'] = $this->cloneVar($trace['options']); diff --git a/src/Symfony/Component/HttpClient/Response/TraceableResponse.php b/src/Symfony/Component/HttpClient/Response/TraceableResponse.php new file mode 100644 index 0000000000..9305e9be94 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Response/TraceableResponse.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Response; + +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\RedirectionException; +use Symfony\Component\HttpClient\Exception\ServerException; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class TraceableResponse implements ResponseInterface +{ + private $client; + private $response; + private $content; + + public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content) + { + $this->client = $client; + $this->response = $response; + $this->content = &$content; + } + + public function getStatusCode(): int + { + return $this->response->getStatusCode(); + } + + public function getHeaders(bool $throw = true): array + { + return $this->response->getHeaders($throw); + } + + public function getContent(bool $throw = true): string + { + $this->content = $this->response->getContent(false); + + if ($throw) { + $this->checkStatusCode($this->response->getStatusCode()); + } + + return $this->content; + } + + public function toArray(bool $throw = true): array + { + $this->content = $this->response->toArray(false); + + if ($throw) { + $this->checkStatusCode($this->response->getStatusCode()); + } + + return $this->content; + } + + public function cancel(): void + { + $this->response->cancel(); + } + + public function getInfo(string $type = null) + { + return $this->response->getInfo($type); + } + + /** + * Casts the response to a PHP stream resource. + * + * @return resource + * + * @throws TransportExceptionInterface When a network error occurs + * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached + * @throws ClientExceptionInterface On a 4xx when $throw is true + * @throws ServerExceptionInterface On a 5xx when $throw is true + */ + public function toStream(bool $throw = true) + { + if ($throw) { + // Ensure headers arrived + $this->response->getHeaders(true); + } + + if (\is_callable([$this->response, 'toStream'])) { + return $this->response->toStream(false); + } + + return StreamWrapper::createResource($this->response, $this->client); + } + + private function checkStatusCode($code) + { + if (500 <= $code) { + throw new ServerException($this); + } + + if (400 <= $code) { + throw new ClientException($this); + } + + if (300 <= $code) { + throw new RedirectionException($this); + } + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php index 949d8afcff..181cc84a36 100755 --- a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php @@ -36,10 +36,10 @@ class TraceableHttpClientTest extends TestCase return true; }) ) - ->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse())) + ->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse('hello'))) ; $sut = new TraceableHttpClient($httpClient); - $sut->request('GET', '/foo/bar', ['options1' => 'foo']); + $sut->request('GET', '/foo/bar', ['options1' => 'foo'])->getContent(); $this->assertCount(1, $tracedRequests = $sut->getTracedRequests()); $actualTracedRequest = $tracedRequests[0]; $this->assertEquals([ @@ -47,6 +47,7 @@ class TraceableHttpClientTest extends TestCase 'url' => '/foo/bar', 'options' => ['options1' => 'foo'], 'info' => [], + 'content' => 'hello', ], $actualTracedRequest); } diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php index 4d2e4830bc..a69398bb3f 100644 --- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\Response\TraceableResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; @@ -36,12 +37,14 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface, */ public function request(string $method, string $url, array $options = []): ResponseInterface { + $content = ''; $traceInfo = []; $this->tracedRequests[] = [ 'method' => $method, 'url' => $url, 'options' => $options, 'info' => &$traceInfo, + 'content' => &$content, ]; $onProgress = $options['on_progress'] ?? null; @@ -53,7 +56,7 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface, } }; - return $this->client->request($method, $url, $options); + return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content); } /** @@ -61,7 +64,21 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface, */ public function stream($responses, float $timeout = null): ResponseStreamInterface { - return $this->client->stream($responses, $timeout); + if ($responses instanceof TraceableResponse) { + $responses = [$responses]; + } elseif (!is_iterable($responses)) { + throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of TraceableResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses))); + } + + return $this->client->stream(\Closure::bind(static function () use ($responses) { + foreach ($responses as $k => $r) { + if (!$r instanceof TraceableResponse) { + throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of TraceableResponse objects, %s given.', __METHOD__, \is_object($r) ? \get_class($r) : \gettype($r))); + } + + yield $k => $r->response; + } + }, null, TraceableResponse::class), $timeout); } public function getTracedRequests(): array diff --git a/src/Symfony/Component/VarDumper/Caster/ImgStub.php b/src/Symfony/Component/VarDumper/Caster/ImgStub.php index 05789fe336..a16681f736 100644 --- a/src/Symfony/Component/VarDumper/Caster/ImgStub.php +++ b/src/Symfony/Component/VarDumper/Caster/ImgStub.php @@ -16,7 +16,7 @@ namespace Symfony\Component\VarDumper\Caster; */ class ImgStub extends ConstStub { - public function __construct(string $data, string $contentType, string $size) + public function __construct(string $data, string $contentType, string $size = '') { $this->value = ''; $this->attr['img-data'] = $data; diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php index 326ce1d863..a840429375 100644 --- a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php @@ -195,7 +195,7 @@ class CliDumper extends AbstractDumper 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0, 'binary' => $bin, ]; - $str = explode("\n", $str); + $str = $bin && false !== strpos($str, "\0") ? [$str] : explode("\n", $str); if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) { unset($str[1]); $str[0] .= "\n"; diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php index a0a2ebcc16..19f949fc58 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php @@ -62,7 +62,7 @@ array:24 [ 6 => {$intMax} "str" => "déjà\\n" 7 => b""" - é\\x00test\\t\\n + é\\x01test\\t\\n ing """ "[]" => [] diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php index b6db08ea9e..211621900f 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php @@ -66,7 +66,7 @@ class HtmlDumperTest extends TestCase 6 => {$intMax} "str" => "d&%s;j&%s;\\n" 7 => b""" - é\\x00test\\t\\n + é\\x01test\\t\\n ing """ "[]" => [] diff --git a/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php b/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php index dcce237243..1361fa4554 100644 --- a/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php +++ b/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php @@ -17,7 +17,7 @@ $g = fopen(__FILE__, 'r'); $var = [ 'number' => 1, null, 'const' => 1.1, true, false, NAN, INF, -INF, PHP_INT_MAX, - 'str' => "déjà\n", "\xE9\x00test\t\ning", + 'str' => "déjà\n", "\xE9\x01test\t\ning", '[]' => [], 'res' => $g, 'obj' => $foo,