Completed
Pull Request — master (#15)
by
unknown
07:42
created

ApiClient::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 16

Duplication

Lines 19
Ratio 100 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 19
loc 19
rs 9.4285
cc 1
eloc 16
nc 1
nop 6
1
<?php declare(strict_types = 1);
2
3
namespace SlevomatCsobGateway\Api;
4
5
use DateTimeImmutable;
6
use Psr\Log\LoggerInterface;
7
use SlevomatCsobGateway\Call\ResponseExtensionHandler;
8
use SlevomatCsobGateway\Crypto\CryptoService;
9
use SlevomatCsobGateway\Crypto\PrivateKeyFileException;
10
use SlevomatCsobGateway\Crypto\PublicKeyFileException;
11
use SlevomatCsobGateway\Crypto\SignatureDataFormatter;
12
use SlevomatCsobGateway\Crypto\SigningFailedException;
13
use SlevomatCsobGateway\Crypto\VerificationFailedException;
14
15
class ApiClient
16
{
17
18
	/**
19
	 * @var ApiClientDriver
20
	 */
21
	private $driver;
22
23
	/**
24
	 * @var CryptoService
25
	 */
26
	private $cryptoService;
27
28
	/**
29
	 * @var LoggerInterface|null
30
	 */
31
	private $logger;
32
33
	/**
34
	 * @var string
35
	 */
36
	private $apiUrl;
37
38
	public function __construct(
39
		ApiClientDriver $driver,
40
		CryptoService $cryptoService,
41
		string $apiUrl = null
42
	)
43
	{
44
		$this->driver = $driver;
45
		$this->cryptoService = $cryptoService;
46
		$this->apiUrl = $apiUrl;
47
	}
48
49
	public function setLogger(LoggerInterface $logger = null)
50
	{
51
		$this->logger = $logger;
52
	}
53
54
	/**
55
	 * @param string $url
56
	 * @param mixed[]|null $data
57
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
58
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
59
	 * @param \Closure|null $responseValidityCallback
60
	 * @param ResponseExtensionHandler[] $extensions
61
	 * @return Response
62
	 *
63
	 * @throws PrivateKeyFileException
64
	 * @throws SigningFailedException
65
	 * @throws PublicKeyFileException
66
	 * @throws VerificationFailedException
67
	 * @throws RequestException
68
	 * @throws ApiClientDriverException
69
	 * @throws InvalidSignatureException
70
	 */
71 View Code Duplication
	public function get(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
72
		string $url,
73
		array $data = [],
74
		SignatureDataFormatter $requestSignatureDataFormatter,
75
		SignatureDataFormatter $responseSignatureDataFormatter,
76
		\Closure $responseValidityCallback = null,
77
		array $extensions = []
78
	): Response
79
	{
80
		return $this->request(
81
			new HttpMethod(HttpMethod::GET),
82
			$url,
83
			$this->prepareData($data, $requestSignatureDataFormatter),
84
			null,
85
			$responseSignatureDataFormatter,
86
			$responseValidityCallback,
87
			$extensions
88
		);
89
	}
90
91
	/**
92
	 * @param string $url
93
	 * @param mixed[]|null $data
94
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
95
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
96
	 * @param ResponseExtensionHandler[] $extensions
97
	 * @return Response
98
	 *
99
	 * @throws PrivateKeyFileException
100
	 * @throws SigningFailedException
101
	 * @throws PublicKeyFileException
102
	 * @throws VerificationFailedException
103
	 * @throws RequestException
104
	 * @throws ApiClientDriverException
105
	 * @throws InvalidSignatureException
106
	 */
107 View Code Duplication
	public function post(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
108
		string $url,
109
		array $data = [],
110
		SignatureDataFormatter $requestSignatureDataFormatter,
111
		SignatureDataFormatter $responseSignatureDataFormatter,
112
		array $extensions = []
113
	): Response
114
	{
115
		return $this->request(
116
			new HttpMethod(HttpMethod::POST),
117
			$url,
118
			[],
119
			$this->prepareData($data, $requestSignatureDataFormatter),
120
			$responseSignatureDataFormatter,
121
			null,
122
			$extensions
123
		);
124
	}
125
126
	/**
127
	 * @param string $url
128
	 * @param mixed[]|null $data
129
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
130
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
131
	 * @param ResponseExtensionHandler[] $extensions
132
	 * @return Response
133
	 *
134
	 * @throws PrivateKeyFileException
135
	 * @throws SigningFailedException
136
	 * @throws PublicKeyFileException
137
	 * @throws VerificationFailedException
138
	 * @throws RequestException
139
	 * @throws ApiClientDriverException
140
	 * @throws InvalidSignatureException
141
	 */
142 View Code Duplication
	public function put(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
143
		string $url,
144
		array $data = [],
145
		SignatureDataFormatter $requestSignatureDataFormatter,
146
		SignatureDataFormatter $responseSignatureDataFormatter,
147
		array $extensions = []
148
	): Response
149
	{
150
		return $this->request(
151
			new HttpMethod(HttpMethod::PUT),
152
			$url,
153
			[],
154
			$this->prepareData($data, $requestSignatureDataFormatter),
155
			$responseSignatureDataFormatter,
156
			null,
157
			$extensions
158
		);
159
	}
160
161
	/**
162
	 * @param HttpMethod $method
163
	 * @param string $url
164
	 * @param string[] $queries
165
	 * @param mixed[]|null $data
166
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
167
	 * @param \Closure|null $responseValidityCallback
168
	 * @param ResponseExtensionHandler[] $extensions
169
	 * @return Response
170
	 *
171
	 * @throws PrivateKeyFileException
172
	 * @throws SigningFailedException
173
	 * @throws PublicKeyFileException
174
	 * @throws VerificationFailedException
175
	 * @throws RequestException
176
	 * @throws ApiClientDriverException
177
	 * @throws InvalidSignatureException
178
	 */
179
	public function request(
180
		HttpMethod $method,
181
		string $url,
182
		array $queries = [],
183
		array $data = null,
184
		SignatureDataFormatter $responseSignatureDataFormatter,
185
		\Closure $responseValidityCallback = null,
186
		array $extensions = []
187
	): Response
188
	{
189
		$urlFirstQueryPosition = strpos($url, '{');
190
		$urlShort = ($urlFirstQueryPosition !== false ? substr($url, 0, $urlFirstQueryPosition) : $url);
191
192
		foreach ($queries as $key => $value) {
193
			if (strpos($url, '{' . $key . '}') !== false) {
194
				$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $url. This often makes code more readable.
Loading history...
195
				unset($queries[$key]);
196
			}
197
		}
198
199
		if ($queries !== []) {
200
			throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
201
		}
202
203
		$response = $this->driver->request(
204
			$method,
205
			$this->apiUrl . '/' . $url,
206
			$data
207
		);
208
209
		$this->logRequest($method, $urlShort, $queries, $data, $response);
210
211
		if ($responseValidityCallback !== null) {
212
			$responseValidityCallback($response);
213
		}
214
215
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
216
			$decodedExtensions = [];
217
			if ($extensions !== [] && array_key_exists('extensions', $response->getData())) {
218
				foreach ($response->getData()['extensions'] as $extensionData) {
219
					$name = $extensionData['extension'];
220
					if (isset($extensions[$name])) {
221
						$handler = $extensions[$name];
222
						$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
223
					}
224
				}
225
			}
226
			$responseData = $this->decodeData($response->getData(), $responseSignatureDataFormatter);
227
			unset($responseData['extensions']);
228
229
			return new Response(
230
				$response->getResponseCode(),
231
				$responseData,
232
				$response->getHeaders(),
233
				$decodedExtensions
234
			);
235
236
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
237
			return new Response(
238
				$response->getResponseCode(),
239
				null,
240
				$response->getHeaders()
241
			);
242
243
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
244
			throw new BadRequestException($response);
245
246
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
247
			throw new ForbiddenException($response);
248
249
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
250
			throw new NotFoundException($response);
251
252
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
253
			throw new MethodNotAllowedException($response);
254
255
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
256
			throw new TooManyRequestsException($response);
257
258
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
259
			throw new ServiceUnavailableException($response);
260
		}
261
262
		throw new InternalErrorException($response);
263
	}
264
265
	/**
266
	 * @param mixed[] $data
267
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
268
	 * @return Response
269
	 *
270
	 * @throws InvalidSignatureException
271
	 * @throws PrivateKeyFileException
272
	 * @throws SigningFailedException
273
	 * @throws PublicKeyFileException
274
	 * @throws VerificationFailedException
275
	 */
276
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
277
	{
278
		$response = new Response(
279
			new ResponseCode(ResponseCode::S200_OK),
280
			$data
281
		);
282
283
		$this->logRequest(new HttpMethod(HttpMethod::GET), 'create-response-by-data', [], $data, $response);
284
285
		return new Response(
286
			$response->getResponseCode(),
287
			$this->decodeData($data, $responseSignatureDataFormatter),
288
			$response->getHeaders()
289
		);
290
	}
291
292
	/**
293
	 * @param mixed[] $data
294
	 * @param SignatureDataFormatter $signatureDataFormatter
295
	 * @return mixed[]
296
	 *
297
	 * @throws PrivateKeyFileException
298
	 * @throws SigningFailedException
299
	 */
300
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
301
	{
302
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
303
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
304
305
		return $data;
306
	}
307
308
	/**
309
	 * @param mixed[] $responseData
310
	 * @param SignatureDataFormatter $signatureDataFormatter
311
	 * @return mixed[]
312
	 *
313
	 * @throws InvalidSignatureException
314
	 * @throws PublicKeyFileException
315
	 * @throws VerificationFailedException
316
	 */
317
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
318
	{
319
		if (!array_key_exists('signature', $responseData)) {
320
			throw new InvalidSignatureException($responseData);
321
		}
322
323
		$signature = $responseData['signature'];
324
		unset($responseData['signature']);
325
326
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
327
			throw new InvalidSignatureException($responseData);
328
		}
329
330
		return $responseData;
331
	}
332
333
	private function logRequest(HttpMethod $method, string $url, array $queries, array $requestData = null, Response $response)
334
	{
335
		if ($this->logger === null) {
336
			return;
337
		}
338
339
		$context = [
340
			'method' => $method->getValue(),
341
			'response' => [
342
				'code' => $response->getResponseCode()->getValue(),
343
				'data' => $response->getData(),
344
			],
345
		];
346
347
		if ($requestData !== null) {
348
			$context['request'] = [
349
				'data' => $requestData,
350
			];
351
		}
352
353
		if (count($queries) !== 0) {
354
			$context['queries'] = $queries;
355
		}
356
357
		unset($context['request']['data']['signature']);
358
		unset($context['response']['data']['signature']);
359
360
		$extensions = $context['response']['data']['extensions'] ?? [];
361
362
		foreach ($extensions as $key => $extensionData) {
363
			unset($context['response']['data']['extensions'][$key]['signature']);
364
		}
365
366
		$this->logger->info($url, $context);
367
	}
368
369
}
370