1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* GuzzleClient.php |
4
|
|
|
* |
5
|
|
|
* @copyright More in license.md |
6
|
|
|
* @license https://www.ipublikuj.eu |
7
|
|
|
* @author Adam Kadlec <[email protected]> |
8
|
|
|
* @package iPublikuj:JsonAPIClient! |
9
|
|
|
* @subpackage Clients |
10
|
|
|
* @since 1.0.0 |
11
|
|
|
* |
12
|
|
|
* @date 05.05.18 |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
declare(strict_types = 1); |
16
|
|
|
|
17
|
|
|
namespace IPub\JsonAPIClient\Clients; |
18
|
|
|
|
19
|
|
|
use Nette; |
20
|
|
|
use Nette\Http as NHttp; |
21
|
|
|
use Nette\Utils; |
22
|
|
|
|
23
|
|
|
use GuzzleHttp; |
24
|
|
|
use GuzzleHttp\Client; |
25
|
|
|
use GuzzleHttp\Exception\BadResponseException; |
26
|
|
|
use GuzzleHttp\Psr7\Request; |
27
|
|
|
|
28
|
|
|
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface; |
29
|
|
|
use Neomerx\JsonApi\Encoder\EncoderOptions; |
30
|
|
|
use Neomerx\JsonApi\Exceptions\JsonApiException; |
31
|
|
|
use Neomerx\JsonApi\Factories\Factory; |
32
|
|
|
|
33
|
|
|
use Psr\Http\Message\RequestInterface as PsrRequest; |
34
|
|
|
use Psr\Http\Message\ResponseInterface as PsrResponse; |
35
|
|
|
|
36
|
|
|
use IPub\JsonAPIClient\Encoders; |
37
|
|
|
use IPub\JsonAPIClient\Exceptions; |
38
|
|
|
use IPub\JsonAPIClient\Http; |
39
|
|
|
use IPub\JsonAPIClient\Objects; |
40
|
|
|
use IPub\JsonAPIClient\Schemas; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Guzzle client service |
44
|
|
|
* |
45
|
|
|
* @package iPublikuj:JsonAPIClient! |
46
|
|
|
* @subpackage Clients |
47
|
|
|
* |
48
|
|
|
* @author Adam Kadlec <[email protected]> |
49
|
|
|
*/ |
50
|
1 |
|
class GuzzleClient implements IClient |
51
|
|
|
{ |
52
|
|
|
/** |
53
|
|
|
* Implement nette smart magic |
54
|
|
|
*/ |
55
|
1 |
|
use Nette\SmartObject; |
56
|
|
|
|
57
|
1 |
|
use TSendsRequests; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Additional request headers |
61
|
|
|
* |
62
|
|
|
* @var string[] |
63
|
|
|
*/ |
64
|
|
|
private $headers = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var Client |
68
|
|
|
*/ |
69
|
|
|
private $http; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @param string|NULL $baseUri |
73
|
|
|
* @param Schemas\SchemaProvider $schemaProvider |
74
|
|
|
* @param Client|NULL $client |
75
|
|
|
*/ |
76
|
|
|
public function __construct( |
77
|
|
|
?string $baseUri = NULL, |
78
|
|
|
Schemas\SchemaProvider $schemaProvider, |
79
|
|
|
Client $client = NULL |
80
|
|
|
) { |
81
|
1 |
|
if ($client === NULL && $baseUri === NULL) { |
82
|
|
|
throw new Exceptions\InvalidStateException('You have to define base_uri or client to be able use api client.'); |
83
|
|
|
} |
84
|
|
|
|
85
|
1 |
|
$this->http = $client !== NULL ? $client : $this->createClient($baseUri); |
86
|
|
|
|
87
|
1 |
|
$factory = new Factory; |
88
|
1 |
|
$this->schemas = $factory->createContainer($schemaProvider->getMapping()); |
89
|
1 |
|
$this->serializer = new Encoders\Encoder($factory, $this->schemas, new EncoderOptions( |
90
|
1 |
|
JSON_PRETTY_PRINT |
91
|
|
|
)); |
92
|
1 |
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* {@inheritdoc} |
96
|
|
|
*/ |
97
|
|
|
public function index(string $endpoint, EncodingParametersInterface $parameters = NULL, array $options = []) : Http\IResponse |
98
|
|
|
{ |
99
|
|
|
$options = $this->mergeOptions([ |
100
|
|
|
GuzzleHttp\RequestOptions::HEADERS => $this->jsonApiHeaders(FALSE), |
101
|
|
|
GuzzleHttp\RequestOptions::QUERY => $parameters ? $this->parseSearchQuery($parameters) : NULL, |
102
|
|
|
], $options); |
103
|
|
|
|
104
|
|
|
return $this->request(NHttp\IRequest::GET, $endpoint, $options); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* {@inheritdoc} |
109
|
|
|
*/ |
110
|
|
|
public function read(string $endpoint, EncodingParametersInterface $parameters = NULL, array $options = []) : Http\IResponse |
111
|
|
|
{ |
112
|
|
|
$options = $this->mergeOptions([ |
113
|
|
|
GuzzleHttp\RequestOptions::HEADERS => $this->jsonApiHeaders(FALSE), |
114
|
|
|
GuzzleHttp\RequestOptions::QUERY => $parameters ? $this->parseQuery($parameters) : NULL, |
115
|
|
|
], $options); |
116
|
|
|
|
117
|
|
|
return $this->request(NHttp\IRequest::GET, $endpoint, $options); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* {@inheritdoc} |
122
|
|
|
*/ |
123
|
|
|
public function create(string $endpoint, $record, EncodingParametersInterface $parameters = NULL, array $options = []) : Http\IResponse |
124
|
|
|
{ |
125
|
|
|
if (!is_object($record)) { |
126
|
|
|
throw new Exceptions\InvalidArgumentException('Provided data entity is not an object.'); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
return $this->sendRecord($endpoint, NHttp\IRequest::POST, $this->serializeRecord($record), $parameters, $options); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* {@inheritdoc} |
134
|
|
|
*/ |
135
|
|
|
public function update(string $endpoint, $record, array $fields = NULL, EncodingParametersInterface $parameters = NULL, array $options = []) : Http\IResponse |
136
|
|
|
{ |
137
|
|
|
if (!is_object($record)) { |
138
|
|
|
throw new Exceptions\InvalidArgumentException('Provided data entity is not an object.'); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $this->sendRecord($endpoint, NHttp\IRequest::PATCH, $this->serializeRecord($record, $fields), $parameters, $options); |
|
|
|
|
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* {@inheritdoc} |
146
|
|
|
*/ |
147
|
|
|
public function delete(string $endpoint, array $options = []) : Http\IResponse |
148
|
|
|
{ |
149
|
|
|
$options = $this->mergeOptions([ |
150
|
|
|
GuzzleHttp\RequestOptions::HEADERS => $this->jsonApiHeaders(FALSE) |
151
|
|
|
], $options); |
152
|
|
|
|
153
|
|
|
return $this->request(NHttp\IRequest::DELETE, $endpoint, $options); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* {@inheritdoc} |
158
|
|
|
*/ |
159
|
|
|
public function addApiKey(string $key) : void |
160
|
|
|
{ |
161
|
|
|
$this->addHeader('X-Api-Key', $key); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* {@inheritdoc} |
166
|
|
|
*/ |
167
|
|
|
public function addAuthorization(string $token) : void |
168
|
|
|
{ |
169
|
|
|
$this->addHeader('Authorization', 'Bearer ' . $token); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* {@inheritdoc} |
174
|
|
|
*/ |
175
|
|
|
public function removeAuthorization() : void |
176
|
|
|
{ |
177
|
|
|
if (isset($this->headers['Authorization'])) { |
178
|
|
|
unset($this->headers['Authorization']); |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* {@inheritdoc} |
184
|
|
|
*/ |
185
|
|
|
public function addHeader(string $header, string $value) : void |
186
|
|
|
{ |
187
|
|
|
$this->headers[$header] = $value; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param string $endpoint |
192
|
|
|
* @param string $method |
193
|
|
|
* @param array $serializedRecord the encoded record |
194
|
|
|
* @param EncodingParametersInterface|NULL $parameters |
195
|
|
|
* @param array $options |
196
|
|
|
* |
197
|
|
|
* @return Http\IResponse |
198
|
|
|
* |
199
|
|
|
* @throws JsonApiException |
200
|
|
|
* @throws GuzzleHttp\Exception\GuzzleException |
201
|
|
|
* @throws Utils\JsonException |
202
|
|
|
*/ |
203
|
|
|
protected function sendRecord(string $endpoint, string $method, array $serializedRecord, EncodingParametersInterface $parameters = NULL, array $options = []) : Http\IResponse |
204
|
|
|
{ |
205
|
|
|
$options = $this->mergeOptions([ |
206
|
|
|
GuzzleHttp\RequestOptions::HEADERS => $this->jsonApiHeaders(TRUE), |
207
|
|
|
GuzzleHttp\RequestOptions::QUERY => $parameters ? $this->parseQuery($parameters) : NULL, |
208
|
|
|
], $options); |
209
|
|
|
|
210
|
|
|
$options['json'] = $serializedRecord; |
211
|
|
|
|
212
|
|
|
return $this->request($method, $endpoint, $options); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @param array $new |
217
|
|
|
* @param array $existing |
218
|
|
|
* |
219
|
|
|
* @return array |
220
|
|
|
*/ |
221
|
|
|
protected function mergeOptions(array $new, array $existing) : array |
222
|
|
|
{ |
223
|
|
|
return array_replace_recursive($new, $existing); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @param string $method |
228
|
|
|
* @param string $uri |
229
|
|
|
* @param array $options |
230
|
|
|
* |
231
|
|
|
* @return Http\IResponse |
232
|
|
|
* |
233
|
|
|
* @throws JsonApiException |
234
|
|
|
* @throws GuzzleHttp\Exception\GuzzleException |
235
|
|
|
* @throws Utils\JsonException |
236
|
|
|
*/ |
237
|
|
|
protected function request(string $method, string $uri, array $options = []) : Http\IResponse |
238
|
|
|
{ |
239
|
|
|
$request = new Request($method, $uri); |
240
|
|
|
|
241
|
|
|
try { |
242
|
|
|
$response = $this->http->send($request, $options); |
243
|
|
|
|
244
|
|
|
} catch (BadResponseException $ex) { |
245
|
|
|
throw $this->parseErrorResponse($request, $ex); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
return new Http\Response($response, $this->createDocumentObject($request, $response)); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Safely parse an error response. |
253
|
|
|
* |
254
|
|
|
* This method wraps decoding the body content of the provided exception, so that |
255
|
|
|
* another exception is not thrown while trying to parse an existing exception. |
256
|
|
|
* |
257
|
|
|
* @param PsrRequest $request |
258
|
|
|
* @param BadResponseException $ex |
259
|
|
|
* |
260
|
|
|
* @return JsonApiException |
261
|
|
|
*/ |
262
|
|
|
private function parseErrorResponse(PsrRequest $request, BadResponseException $ex) : JsonApiException |
263
|
|
|
{ |
264
|
|
|
try { |
265
|
|
|
$response = $ex->getResponse(); |
266
|
|
|
|
267
|
|
|
$document = $response ? $this->createDocumentObject($request, $response) : NULL; |
268
|
|
|
|
269
|
|
|
$errors = $document && $document->getErrors() !== NULL ? $document->getErrors() : [$this->createErrorObject($request, $response)]; |
270
|
|
|
|
271
|
|
|
$statusCode = $response ? $response->getStatusCode() : 0; |
272
|
|
|
|
273
|
|
|
} catch (\Exception $e) { |
274
|
|
|
$errors = []; |
275
|
|
|
$statusCode = 0; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
return new JsonApiException($errors, $statusCode, $ex); |
|
|
|
|
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* @param PsrRequest $request |
283
|
|
|
* @param PsrResponse|NULL $response |
284
|
|
|
* |
285
|
|
|
* @return Objects\IDocument|NULL |
286
|
|
|
* |
287
|
|
|
* @throws Utils\JsonException |
288
|
|
|
*/ |
289
|
|
|
private function createDocumentObject(PsrRequest $request, PsrResponse $response = NULL) : ?Objects\IDocument |
290
|
|
|
{ |
291
|
|
|
if (!$this->httpContainsBody($request, $response)) { |
292
|
|
|
return NULL; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
return new Objects\Document(Utils\Json::decode(($response ? (string) $response->getBody() : (string) $request->getBody()))); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* @param PsrRequest $request |
300
|
|
|
* @param PsrResponse|NULL $response |
301
|
|
|
* |
302
|
|
|
* @return Objects\IMutableError|NULL |
303
|
|
|
* |
304
|
|
|
* @throws Utils\JsonException |
305
|
|
|
*/ |
306
|
|
|
private function createErrorObject(PsrRequest $request, PsrResponse $response = NULL) : ?Objects\IMutableError |
307
|
|
|
{ |
308
|
|
|
if (!$this->httpContainsBody($request, $response)) { |
309
|
|
|
return NULL; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
return Objects\Error::create(Utils\Json::decode(($response ? (string) $response->getBody() : (string) $request->getBody()), Utils\Json::FORCE_ARRAY)); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* @param PsrRequest $request |
317
|
|
|
* @param PsrResponse|NULL $response |
318
|
|
|
* |
319
|
|
|
* @return bool |
320
|
|
|
*/ |
321
|
|
|
private function httpContainsBody(PsrRequest $request, ?PsrResponse $response = NULL) : bool |
322
|
|
|
{ |
323
|
|
|
return $response ? |
324
|
|
|
$this->doesResponseHaveBody($request, $response) : |
325
|
|
|
$this->doesRequestHaveBody($request); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Does the HTTP request contain body content? |
330
|
|
|
* |
331
|
|
|
* The presence of a message-body in a request is signaled by the inclusion of a Content-Length or |
332
|
|
|
* Transfer-Encoding header field in the request's message-headers. |
333
|
|
|
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 |
334
|
|
|
* |
335
|
|
|
* However, some browsers send a Content-Length header with an empty string for e.g. GET requests |
336
|
|
|
* without any message-body. Therefore rather than checking for the existence of a Content-Length |
337
|
|
|
* header, we will allow an empty value to indicate that the request does not contain body. |
338
|
|
|
* |
339
|
|
|
* @param PsrRequest $request |
340
|
|
|
* |
341
|
|
|
* @return bool |
342
|
|
|
*/ |
343
|
|
|
private function doesRequestHaveBody(PsrRequest $request) : bool |
344
|
|
|
{ |
345
|
|
|
if ($request->hasHeader('Transfer-Encoding')) { |
346
|
|
|
return TRUE; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
if (!$contentLength = $request->getHeader('Content-Length')) { |
350
|
|
|
return FALSE; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
return 0 < $contentLength[0]; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Does the HTTP response contain body content? |
358
|
|
|
* |
359
|
|
|
* For response messages, whether or not a message-body is included with a message is dependent |
360
|
|
|
* on both the request method and the response status code (section 6.1.1). All responses to the |
361
|
|
|
* HEAD request method MUST NOT include a message-body, even though the presence of entity-header |
362
|
|
|
* fields might lead one to believe they do. All 1xx (informational), 204 (no content), and 304 |
363
|
|
|
* (not modified) responses MUST NOT include a message-body. All other responses do include a |
364
|
|
|
* message-body, although it MAY be of zero length. |
365
|
|
|
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 |
366
|
|
|
* |
367
|
|
|
* @param PsrRequest $request |
368
|
|
|
* @param PsrResponse $response |
369
|
|
|
* |
370
|
|
|
* @return bool |
371
|
|
|
*/ |
372
|
|
|
private function doesResponseHaveBody(PsrRequest $request, PsrResponse $response) : bool |
373
|
|
|
{ |
374
|
|
|
if (strtoupper($request->getMethod()) === 'HEAD') { |
375
|
|
|
return FALSE; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
$status = $response->getStatusCode(); |
379
|
|
|
|
380
|
|
|
if ((100 <= $status && 200 > $status) || 204 === $status || 304 === $status) { |
381
|
|
|
return FALSE; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
if ($response->hasHeader('Transfer-Encoding')) { |
385
|
|
|
return TRUE; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
if (!$contentLength = $response->getHeader('Content-Length')) { |
389
|
|
|
return FALSE; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
return 0 < $contentLength[0]; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* @param string $baseUri |
397
|
|
|
* |
398
|
|
|
* @return Client |
399
|
|
|
*/ |
400
|
|
|
private function createClient(string $baseUri) : Client |
401
|
|
|
{ |
402
|
1 |
|
$client = new Client([ |
403
|
1 |
|
'base_uri' => $baseUri, |
404
|
|
|
]); |
405
|
|
|
|
406
|
1 |
|
return $client; |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.