feature #30499 [HttpClient] add ResponseInterface::toArray() (nicolas-grekas)
This PR was merged into the 4.3-dev branch.
Discussion
----------
[HttpClient] add ResponseInterface::toArray()
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
I'd like we discuss adding a `toArray()` method to `ResponseInterface`.
JSON responses are so common when doing server-side requests that this may help remove boilerplate - especially the logic dealing with errors.
WDYT?
(about flags, I don't think we should make them configurable: if one really needs to deal with custom flags, there's always `ResponseInterface::getContent()` - but it should be very rare.).
Commits
-------
aabd1d455e
[HttpClient] add ResponseInterface::toArray()
This commit is contained in:
commit
4619ae483d
@ -34,7 +34,7 @@
|
|||||||
"symfony/polyfill-intl-idn": "^1.10",
|
"symfony/polyfill-intl-idn": "^1.10",
|
||||||
"symfony/polyfill-mbstring": "~1.0",
|
"symfony/polyfill-mbstring": "~1.0",
|
||||||
"symfony/polyfill-php72": "~1.5",
|
"symfony/polyfill-php72": "~1.5",
|
||||||
"symfony/polyfill-php73": "^1.8"
|
"symfony/polyfill-php73": "^1.11"
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
"symfony/asset": "self.version",
|
"symfony/asset": "self.version",
|
||||||
|
25
src/Symfony/Component/HttpClient/Exception/JsonException.php
Normal file
25
src/Symfony/Component/HttpClient/Exception/JsonException.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Exception;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by responses' toArray() method when their content cannot be JSON-decoded.
|
||||||
|
*
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class JsonException extends \JsonException implements TransportExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
@ -15,6 +15,7 @@ use Symfony\Component\HttpClient\Chunk\DataChunk;
|
|||||||
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
||||||
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
||||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||||
|
use Symfony\Component\HttpClient\Exception\JsonException;
|
||||||
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
||||||
use Symfony\Component\HttpClient\Exception\ServerException;
|
use Symfony\Component\HttpClient\Exception\ServerException;
|
||||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
@ -52,6 +53,7 @@ trait ResponseTrait
|
|||||||
private $timeout;
|
private $timeout;
|
||||||
private $finalInfo;
|
private $finalInfo;
|
||||||
private $offset = 0;
|
private $offset = 0;
|
||||||
|
private $jsonData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
@ -121,6 +123,47 @@ trait ResponseTrait
|
|||||||
return stream_get_contents($this->content);
|
return stream_get_contents($this->content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toArray(bool $throw = true): array
|
||||||
|
{
|
||||||
|
if ('' === $content = $this->getContent($throw)) {
|
||||||
|
throw new TransportException('Response body is empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->jsonData) {
|
||||||
|
return $this->jsonData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentType = $this->headers['content-type'][0] ?? 'application/json';
|
||||||
|
|
||||||
|
if (!preg_match('/\bjson\b/i', $contentType)) {
|
||||||
|
throw new JsonException(sprintf('Response content-type is "%s" while a JSON-compatible one was expected.', $contentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$content = json_decode($content, true, 512, JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? JSON_THROW_ON_ERROR : 0));
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
throw new JsonException($e->getMessage(), $e->getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error()) {
|
||||||
|
throw new JsonException(json_last_error_msg(), json_last_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_array($content)) {
|
||||||
|
throw new JsonException(sprintf('JSON content was expected to decode to an array, %s returned.', \gettype($content)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->content) {
|
||||||
|
// Option "buffer" is true
|
||||||
|
return $this->jsonData = $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the response and all its network handles.
|
* Closes the response and all its network handles.
|
||||||
*/
|
*/
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"symfony/contracts": "^1.1"
|
"symfony/contracts": "^1.1",
|
||||||
|
"symfony/polyfill-php73": "^1.11"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"nyholm/psr7": "^1.0",
|
"nyholm/psr7": "^1.0",
|
||||||
|
@ -59,6 +59,18 @@ interface ResponseInterface
|
|||||||
*/
|
*/
|
||||||
public function getContent(bool $throw = true): string;
|
public function getContent(bool $throw = true): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the response body decoded as array, typically from a JSON payload.
|
||||||
|
*
|
||||||
|
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
|
||||||
|
*
|
||||||
|
* @throws TransportExceptionInterface When the body cannot be decoded or 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 toArray(bool $throw = true): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns info coming from the transport layer.
|
* Returns info coming from the transport layer.
|
||||||
*
|
*
|
||||||
|
@ -58,6 +58,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->assertSame(['application/json'], $headers['content-type']);
|
$this->assertSame(['application/json'], $headers['content-type']);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = json_decode($response->getContent(), true);
|
||||||
|
$this->assertSame($body, $response->toArray());
|
||||||
|
|
||||||
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
|
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
|
||||||
$this->assertSame('/', $body['REQUEST_URI']);
|
$this->assertSame('/', $body['REQUEST_URI']);
|
||||||
@ -79,7 +80,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'headers' => ['Foo' => 'baR'],
|
'headers' => ['Foo' => 'baR'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
$this->assertSame('baR', $body['HTTP_FOO']);
|
$this->assertSame('baR', $body['HTTP_FOO']);
|
||||||
|
|
||||||
$this->expectException(TransportExceptionInterface::class);
|
$this->expectException(TransportExceptionInterface::class);
|
||||||
@ -106,7 +107,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->assertSame(200, $response->getStatusCode());
|
$this->assertSame(200, $response->getStatusCode());
|
||||||
$this->assertSame('HTTP/1.0 200 OK', $response->getInfo('raw_headers')[0]);
|
$this->assertSame('HTTP/1.0 200 OK', $response->getInfo('raw_headers')[0]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame('HTTP/1.0', $body['SERVER_PROTOCOL']);
|
$this->assertSame('HTTP/1.0', $body['SERVER_PROTOCOL']);
|
||||||
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
||||||
@ -203,7 +204,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$client = $this->getHttpClient();
|
$client = $this->getHttpClient();
|
||||||
$response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057');
|
$response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057');
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame('foo', $body['PHP_AUTH_USER']);
|
$this->assertSame('foo', $body['PHP_AUTH_USER']);
|
||||||
$this->assertSame('bar=bar', $body['PHP_AUTH_PW']);
|
$this->assertSame('bar=bar', $body['PHP_AUTH_PW']);
|
||||||
@ -219,7 +220,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
||||||
$this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
|
$this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
|
||||||
$this->assertSame('http://localhost:8057/', $response->getInfo('url'));
|
$this->assertSame('http://localhost:8057/', $response->getInfo('url'));
|
||||||
@ -250,7 +251,8 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$client = $this->getHttpClient();
|
$client = $this->getHttpClient();
|
||||||
$response = $client->request('GET', 'http://localhost:8057/302/relative');
|
$response = $client->request('GET', 'http://localhost:8057/302/relative');
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame('/', $body['REQUEST_URI']);
|
$this->assertSame('/', $body['REQUEST_URI']);
|
||||||
$this->assertNull($response->getInfo('redirect_url'));
|
$this->assertNull($response->getInfo('redirect_url'));
|
||||||
|
|
||||||
@ -279,7 +281,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'body' => 'foo=bar',
|
'body' => 'foo=bar',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
|
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
|
||||||
}
|
}
|
||||||
@ -388,7 +390,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
|
'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
|
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
|
||||||
$this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
|
$this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
|
||||||
@ -405,7 +407,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'body' => ['foo' => 'bar'],
|
'body' => ['foo' => 'bar'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], json_decode($response->getContent(), true));
|
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $response->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPostResource()
|
public function testPostResource()
|
||||||
@ -420,7 +422,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'body' => $h,
|
'body' => $h,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
|
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
|
||||||
}
|
}
|
||||||
@ -438,7 +440,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], json_decode($response->getContent(), true));
|
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $response->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOnProgressCancel()
|
public function testOnProgressCancel()
|
||||||
@ -581,7 +583,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'proxy' => 'http://localhost:8057',
|
'proxy' => 'http://localhost:8057',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
$this->assertSame('localhost:8057', $body['HTTP_HOST']);
|
$this->assertSame('localhost:8057', $body['HTTP_HOST']);
|
||||||
$this->assertRegexp('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
|
$this->assertRegexp('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
|
||||||
|
|
||||||
@ -589,7 +591,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'proxy' => 'http://foo:b%3Dar@localhost:8057',
|
'proxy' => 'http://foo:b%3Dar@localhost:8057',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
$this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
|
$this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +605,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'proxy' => 'http://localhost:8057',
|
'proxy' => 'http://localhost:8057',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
|
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
|
||||||
$this->assertSame('/', $body['REQUEST_URI']);
|
$this->assertSame('/', $body['REQUEST_URI']);
|
||||||
@ -629,7 +631,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->assertSame(['Accept-Encoding'], $headers['vary']);
|
$this->assertSame(['Accept-Encoding'], $headers['vary']);
|
||||||
$this->assertContains('gzip', $headers['content-encoding'][0]);
|
$this->assertContains('gzip', $headers['content-encoding'][0]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
|
|
||||||
$this->assertContains('gzip', $body['HTTP_ACCEPT_ENCODING']);
|
$this->assertContains('gzip', $body['HTTP_ACCEPT_ENCODING']);
|
||||||
}
|
}
|
||||||
@ -652,7 +654,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'query' => ['b' => 'b'],
|
'query' => ['b' => 'b'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = $response->toArray();
|
||||||
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
$this->assertSame('GET', $body['REQUEST_METHOD']);
|
||||||
$this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
|
$this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
|
||||||
}
|
}
|
||||||
@ -673,10 +675,9 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->assertContains('gzip', $headers['content-encoding'][0]);
|
$this->assertContains('gzip', $headers['content-encoding'][0]);
|
||||||
|
|
||||||
$body = $response->getContent();
|
$body = $response->getContent();
|
||||||
|
|
||||||
$this->assertSame("\x1F", $body[0]);
|
$this->assertSame("\x1F", $body[0]);
|
||||||
$body = json_decode(gzdecode($body), true);
|
|
||||||
|
|
||||||
|
$body = json_decode(gzdecode($body), true);
|
||||||
$this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
|
$this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user