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:
Nicolas Grekas 2019-03-10 18:25:55 +01:00
commit 4619ae483d
6 changed files with 101 additions and 19 deletions

View File

@ -34,7 +34,7 @@
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php72": "~1.5",
"symfony/polyfill-php73": "^1.8"
"symfony/polyfill-php73": "^1.11"
},
"replace": {
"symfony/asset": "self.version",

View 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
{
}

View File

@ -15,6 +15,7 @@ use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\TransportException;
@ -52,6 +53,7 @@ trait ResponseTrait
private $timeout;
private $finalInfo;
private $offset = 0;
private $jsonData;
/**
* {@inheritdoc}
@ -121,6 +123,47 @@ trait ResponseTrait
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.
*/

View File

@ -20,7 +20,8 @@
},
"require": {
"php": "^7.1.3",
"symfony/contracts": "^1.1"
"symfony/contracts": "^1.1",
"symfony/polyfill-php73": "^1.11"
},
"require-dev": {
"nyholm/psr7": "^1.0",

View File

@ -59,6 +59,18 @@ interface ResponseInterface
*/
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.
*

View File

@ -58,6 +58,7 @@ abstract class HttpClientTestCase extends TestCase
$this->assertSame(['application/json'], $headers['content-type']);
$body = json_decode($response->getContent(), true);
$this->assertSame($body, $response->toArray());
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
$this->assertSame('/', $body['REQUEST_URI']);
@ -79,7 +80,7 @@ abstract class HttpClientTestCase extends TestCase
'headers' => ['Foo' => 'baR'],
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('baR', $body['HTTP_FOO']);
$this->expectException(TransportExceptionInterface::class);
@ -106,7 +107,7 @@ abstract class HttpClientTestCase extends TestCase
$this->assertSame(200, $response->getStatusCode());
$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('GET', $body['REQUEST_METHOD']);
@ -203,7 +204,7 @@ abstract class HttpClientTestCase extends TestCase
$client = $this->getHttpClient();
$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('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('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
$this->assertSame('http://localhost:8057/', $response->getInfo('url'));
@ -250,7 +251,8 @@ abstract class HttpClientTestCase extends TestCase
$client = $this->getHttpClient();
$response = $client->request('GET', 'http://localhost:8057/302/relative');
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('/', $body['REQUEST_URI']);
$this->assertNull($response->getInfo('redirect_url'));
@ -279,7 +281,7 @@ abstract class HttpClientTestCase extends TestCase
'body' => 'foo=bar',
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$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; },
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
$this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
@ -405,7 +407,7 @@ abstract class HttpClientTestCase extends TestCase
'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()
@ -420,7 +422,7 @@ abstract class HttpClientTestCase extends TestCase
'body' => $h,
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$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()
@ -581,7 +583,7 @@ abstract class HttpClientTestCase extends TestCase
'proxy' => 'http://localhost:8057',
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('localhost:8057', $body['HTTP_HOST']);
$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',
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
}
@ -603,7 +605,7 @@ abstract class HttpClientTestCase extends TestCase
'proxy' => 'http://localhost:8057',
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
$this->assertSame('/', $body['REQUEST_URI']);
@ -629,7 +631,7 @@ abstract class HttpClientTestCase extends TestCase
$this->assertSame(['Accept-Encoding'], $headers['vary']);
$this->assertContains('gzip', $headers['content-encoding'][0]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertContains('gzip', $body['HTTP_ACCEPT_ENCODING']);
}
@ -652,7 +654,7 @@ abstract class HttpClientTestCase extends TestCase
'query' => ['b' => 'b'],
]);
$body = json_decode($response->getContent(), true);
$body = $response->toArray();
$this->assertSame('GET', $body['REQUEST_METHOD']);
$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]);
$body = $response->getContent();
$this->assertSame("\x1F", $body[0]);
$body = json_decode(gzdecode($body), true);
$body = json_decode(gzdecode($body), true);
$this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
}