Passed
Push — master ( 3e64fc...876719 )
by Adam
04:04
created

GuzzleClient   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 18

Test Coverage

Coverage 15.66%

Importance

Changes 0
Metric Value
wmc 49
lcom 2
cbo 18
dl 0
loc 359
ccs 13
cts 83
cp 0.1566
rs 8.48
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 4
A index() 0 9 2
A read() 0 9 2
A create() 0 8 2
A update() 0 8 2
A delete() 0 8 1
A addApiKey() 0 4 1
A addAuthorization() 0 4 1
A removeAuthorization() 0 6 2
A addHeader() 0 4 1
A sendRecord() 0 11 2
A mergeOptions() 0 4 1
A request() 0 13 2
A parseErrorResponse() 0 18 6
A createDocumentObject() 0 8 3
A createErrorObject() 0 8 3
A httpContainsBody() 0 6 2
A doesRequestHaveBody() 0 12 3
B doesResponseHaveBody() 0 22 8
A createClient() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like GuzzleClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GuzzleClient, and based on these observations, apply Extract Interface, too.

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);
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 135 can also be of type array; however, IPub\JsonAPIClient\Clien...ests::serializeRecord() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $errors can also be of type array; however, Neomerx\JsonApi\Exceptio...xception::__construct() does only seem to accept object<Neomerx\JsonApi\C...ptions\ErrorCollection>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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