Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6476478

Browse files
[HttpClient] add ApiClient et al.
1 parent ca05293 commit 6476478

File tree

6 files changed

+393
-2
lines changed

6 files changed

+393
-2
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"symfony/polyfill-intl-idn": "^1.10",
3535
"symfony/polyfill-mbstring": "~1.0",
3636
"symfony/polyfill-php72": "~1.5",
37-
"symfony/polyfill-php73": "^1.8"
37+
"symfony/polyfill-php73": "^1.11"
3838
},
3939
"replace": {
4040
"symfony/asset": "self.version",
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\HttpClient\Response\ApiResponse;
15+
use Symfony\Component\HttpClient\Response\ApiResponseStream;
16+
use Symfony\Contracts\HttpClient\ApiClientInterface;
17+
use Symfony\Contracts\HttpClient\ApiResponseInterface;
18+
use Symfony\Contracts\HttpClient\ApiResponseStreamInterface;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* @see ApiClientInterface::OPTIONS_DEFAULTS for available options
23+
*
24+
* @author Nicolas Grekas <[email protected]>
25+
*/
26+
final class ApiClient implements ApiClientInterface
27+
{
28+
use HttpClientTrait;
29+
30+
private $client;
31+
private $defaultOptions = [
32+
'headers' => [
33+
'accept' => ['application/json'],
34+
],
35+
] + ApiClientInterface::OPTIONS_DEFAULTS;
36+
37+
/**
38+
* A factory to instantiate the best possible API client for the runtime.
39+
*
40+
* @param array $defaultOptions Default requests' options
41+
* @param int $maxHostConnections The maximum number of connections to a single host
42+
*
43+
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
44+
*/
45+
public static function create(array $defaultOptions = [], int $maxHostConnections = 6): ApiClientInterface
46+
{
47+
return new self(HttpClient::create($defaultOptions, $maxHostConnections));
48+
}
49+
50+
public function __construct(HttpClientInterface $client, array $defaultOptions = [])
51+
{
52+
$this->client = $client;
53+
54+
if ($defaultOptions) {
55+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
56+
}
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function get(string $url, iterable $options = []): ApiResponseInterface
63+
{
64+
return $this->requestApi('GET', $url, $options);
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
public function head(string $url, iterable $options = []): ApiResponseInterface
71+
{
72+
return $this->requestApi('HEAD', $url, $options);
73+
}
74+
75+
/**
76+
* {@inheritdoc}
77+
*/
78+
public function post(string $url, iterable $options = []): ApiResponseInterface
79+
{
80+
return $this->requestApi('POST', $url, $options);
81+
}
82+
83+
/**
84+
* {@inheritdoc}
85+
*/
86+
public function put(string $url, iterable $options = []): ApiResponseInterface
87+
{
88+
return $this->requestApi('PUT', $url, $options);
89+
}
90+
91+
/**
92+
* {@inheritdoc}
93+
*/
94+
public function patch(string $url, iterable $options = []): ApiResponseInterface
95+
{
96+
return $this->requestApi('PATCH', $url, $options);
97+
}
98+
99+
/**
100+
* {@inheritdoc}
101+
*/
102+
public function delete(string $url, iterable $options = []): ApiResponseInterface
103+
{
104+
return $this->requestApi('DELETE', $url, $options);
105+
}
106+
107+
/**
108+
* {@inheritdoc}
109+
*/
110+
public function options(string $url, iterable $options = []): ApiResponseInterface
111+
{
112+
return $this->requestApi('OPTIONS', $url, $options);
113+
}
114+
115+
/**
116+
* {@inheritdoc}
117+
*/
118+
public function complete($responses, float $timeout = null): ApiResponseStreamInterface
119+
{
120+
if ($responses instanceof ApiResponse) {
121+
$responses = [$responses];
122+
} elseif (!\is_iterable($responses)) {
123+
throw new \TypeError(sprintf('%s() expects parameter 1 to be iterable of ApiResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
124+
}
125+
126+
return new ApiResponseStream(ApiResponse::complete($this->client, $responses, $timeout));
127+
}
128+
129+
private function requestApi(string $method, string $url, iterable $options): ApiResponse
130+
{
131+
$options['buffer'] = true;
132+
[, $options] = self::prepareRequest(null, null, $options, $this->defaultOptions);
133+
134+
return new ApiResponse($this->client->request($method, $url, $options));
135+
}
136+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Exception;
13+
14+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
15+
16+
/**
17+
* @author Nicolas Grekas <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
final class JsonException extends \JsonException implements TransportExceptionInterface
22+
{
23+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Response;
13+
14+
use Symfony\Component\HttpClient\Exception\JsonException;
15+
use Symfony\Component\HttpClient\Exception\TransportException;
16+
use Symfony\Contracts\HttpClient\ApiResponseInterface;
17+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
/**
22+
* @author Nicolas Grekas <[email protected]>
23+
*
24+
* @internal
25+
*/
26+
final class ApiResponse implements ApiResponseInterface
27+
{
28+
private $response;
29+
private $data;
30+
private $error;
31+
private $timeout;
32+
33+
/**
34+
* @internal
35+
*/
36+
public function __construct(ResponseInterface $response)
37+
{
38+
$this->response = $response;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function getStatusCode(): int
45+
{
46+
$this->clearError();
47+
48+
return $this->response->getStatusCode();
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function getHeaders(bool $throw = true): array
55+
{
56+
$this->clearError();
57+
58+
return $this->response->getHeaders($throw);
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function getContent(bool $throw = true): string
65+
{
66+
$this->clearError();
67+
68+
return $this->response->getContent($throw);
69+
}
70+
71+
/**
72+
* {@inheritdoc}
73+
*/
74+
public function getInfo(string $type = null)
75+
{
76+
return $this->response->getInfo($type);
77+
}
78+
79+
/**
80+
* {@inheritdoc}
81+
*/
82+
public function toArray(bool $throw = true): array
83+
{
84+
$this->clearError();
85+
$contentType = $this->response->getHeaders($throw)['content-type'][0] ?? 'application/json';
86+
87+
if (null !== $this->data) {
88+
return $this->data;
89+
}
90+
91+
if ('' === $data = $this->response->getContent(false)) {
92+
throw new TransportException('API returned empty response body.');
93+
}
94+
95+
if (!preg_match('/\bjson\b/i', $contentType)) {
96+
throw new JsonException(sprintf('API returned content-type "%s" while a JSON-compatible one was expected.', $contentType));
97+
}
98+
99+
try {
100+
$data = json_decode($data, true, 512, JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? JSON_THROW_ON_ERROR : 0));
101+
} catch (\JsonException $e) {
102+
throw new JsonException($e->getMessage(), $e->getCode(), $e);
103+
}
104+
105+
if (\PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error()) {
106+
throw new JsonException(json_last_error_msg(), json_last_error());
107+
}
108+
109+
if (!\is_array($data)) {
110+
throw new JsonException(sprintf('API returned %s while an array was expected.', \gettype($data)));
111+
}
112+
113+
return $this->data = $data;
114+
}
115+
116+
public function __destruct()
117+
{
118+
if ($this->timeout) {
119+
throw new TransportException('API response reached the inactivity timeout.');
120+
}
121+
122+
$this->clearError();
123+
}
124+
125+
private function clearError()
126+
{
127+
$e = $this->error;
128+
$this->timeout = $this->error = null;
129+
130+
if ($e) {
131+
throw new TransportException($e->getMessage(), 0, $e);
132+
}
133+
}
134+
135+
/**
136+
* @param ApiResponse[] $apiResponses
137+
*
138+
* @internal
139+
*/
140+
public static function complete(HttpClientInterface $client, iterable $apiResponses, ?float $timeout): \Generator
141+
{
142+
$responses = new \SplObjectStorage();
143+
144+
foreach ($apiResponses as $r) {
145+
if (!$r instanceof self) {
146+
throw new \TypeError(sprintf('ApiClient::complete() expects parameter $responses to be iterable of ApiResponse objects, %s given.', __METHOD__, \is_object($r) ? \get_class($r) : \gettype($r)));
147+
}
148+
149+
$responses[$r->response] = $r;
150+
}
151+
152+
foreach ($client->stream($responses, $timeout) as $response => $chunk) {
153+
$r = $responses[$response];
154+
155+
try {
156+
// Skip timed out responses but throw on destruct if unchecked
157+
$r->timeout = $chunk->isTimeout();
158+
159+
if ($r->timeout || !$chunk->isLast()) {
160+
continue;
161+
}
162+
} catch (TransportExceptionInterface $e) {
163+
// Ensure errors are always thrown, last resort on destruct
164+
$r->error = $e;
165+
// Ensure PHP can clear memory as early as possible
166+
$e = null;
167+
}
168+
169+
unset($responses[$response]);
170+
171+
yield $r;
172+
173+
$r->clearError();
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)