ApiClient::logRequest()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.0218

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 20
c 2
b 0
f 0
nc 3
nop 6
dl 0
loc 32
rs 9.6
ccs 16
cts 18
cp 0.8889
crap 4.0218
1
<?php declare(strict_types = 1);
2
3
namespace SlevomatCsobGateway\Api;
4
5
use Closure;
6
use DateTimeImmutable;
7
use InvalidArgumentException;
8
use Psr\Log\LoggerInterface;
9
use SlevomatCsobGateway\Call\ResponseExtensionHandler;
10
use SlevomatCsobGateway\Crypto\CryptoService;
11
use SlevomatCsobGateway\Crypto\PrivateKeyFileException;
12
use SlevomatCsobGateway\Crypto\PublicKeyFileException;
13
use SlevomatCsobGateway\Crypto\SignatureDataFormatter;
14
use SlevomatCsobGateway\Crypto\SigningFailedException;
15
use SlevomatCsobGateway\Crypto\VerificationFailedException;
16
use function array_key_exists;
17
use function json_encode;
18
use function microtime;
19
use function str_replace;
20
use function strpos;
21
use function substr;
22
use function urlencode;
23
24
class ApiClient
25
{
26
27
	/** @var ApiClientDriver */
28
	private $driver;
29
30
	/** @var CryptoService */
31
	private $cryptoService;
32
33
	/** @var LoggerInterface|null */
34
	private $logger;
35
36
	/** @var string|null */
37
	private $apiUrl;
38
39 16
	public function __construct(
40
		ApiClientDriver $driver,
41
		CryptoService $cryptoService,
42
		string $apiUrl
43
	)
44
	{
45 16
		$this->driver = $driver;
46 16
		$this->cryptoService = $cryptoService;
47 16
		$this->apiUrl = $apiUrl;
48 16
	}
49
50 5
	public function setLogger(?LoggerInterface $logger): void
51
	{
52 5
		$this->logger = $logger;
53 5
	}
54
55
	/**
56
	 * @param string $url
57
	 * @param mixed[] $data
58
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
59
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
60
	 * @param Closure|null $responseValidityCallback
61
	 * @param ResponseExtensionHandler[] $extensions
62
	 * @return Response
63
	 *
64
	 * @throws PrivateKeyFileException
65
	 * @throws SigningFailedException
66
	 * @throws PublicKeyFileException
67
	 * @throws VerificationFailedException
68
	 * @throws RequestException
69
	 * @throws ApiClientDriverException
70
	 * @throws InvalidSignatureException
71
	 */
72 13
	public function get(
73
		string $url,
74
		array $data,
75
		SignatureDataFormatter $requestSignatureDataFormatter,
76
		SignatureDataFormatter $responseSignatureDataFormatter,
77
		?Closure $responseValidityCallback = null,
78
		array $extensions = []
79
	): Response
80
	{
81 13
		return $this->request(
82 13
			HttpMethod::get(HttpMethod::GET),
83
			$url,
84 13
			$this->prepareData($data, $requestSignatureDataFormatter),
85 13
			null,
86
			$responseSignatureDataFormatter,
87
			$responseValidityCallback,
88
			$extensions
89
		);
90
	}
91
92
	/**
93
	 * @param string $url
94
	 * @param mixed[] $data
95
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
96
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
97
	 * @param ResponseExtensionHandler[] $extensions
98
	 * @return Response
99
	 *
100
	 * @throws PrivateKeyFileException
101
	 * @throws SigningFailedException
102
	 * @throws PublicKeyFileException
103
	 * @throws VerificationFailedException
104
	 * @throws RequestException
105
	 * @throws ApiClientDriverException
106
	 * @throws InvalidSignatureException
107
	 */
108 1
	public function post(
109
		string $url,
110
		array $data,
111
		SignatureDataFormatter $requestSignatureDataFormatter,
112
		SignatureDataFormatter $responseSignatureDataFormatter,
113
		array $extensions = []
114
	): Response
115
	{
116 1
		return $this->request(
117 1
			HttpMethod::get(HttpMethod::POST),
118
			$url,
119 1
			[],
120 1
			$this->prepareData($data, $requestSignatureDataFormatter),
121
			$responseSignatureDataFormatter,
122 1
			null,
123
			$extensions
124
		);
125
	}
126
127
	/**
128
	 * @param string $url
129
	 * @param mixed[] $data
130
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
131
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
132
	 * @param ResponseExtensionHandler[] $extensions
133
	 * @return Response
134
	 *
135
	 * @throws PrivateKeyFileException
136
	 * @throws SigningFailedException
137
	 * @throws PublicKeyFileException
138
	 * @throws VerificationFailedException
139
	 * @throws RequestException
140
	 * @throws ApiClientDriverException
141
	 * @throws InvalidSignatureException
142
	 */
143 1
	public function put(
144
		string $url,
145
		array $data,
146
		SignatureDataFormatter $requestSignatureDataFormatter,
147
		SignatureDataFormatter $responseSignatureDataFormatter,
148
		array $extensions = []
149
	): Response
150
	{
151 1
		return $this->request(
152 1
			HttpMethod::get(HttpMethod::PUT),
153
			$url,
154 1
			[],
155 1
			$this->prepareData($data, $requestSignatureDataFormatter),
156
			$responseSignatureDataFormatter,
157 1
			null,
158
			$extensions
159
		);
160
	}
161
162
	/**
163
	 * @param HttpMethod $method
164
	 * @param string $url
165
	 * @param mixed[] $queries
166
	 * @param mixed[]|null $data
167
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
168
	 * @param Closure|null $responseValidityCallback
169
	 * @param ResponseExtensionHandler[] $extensions
170
	 * @return Response
171
	 *
172
	 * @throws PrivateKeyFileException
173
	 * @throws SigningFailedException
174
	 * @throws PublicKeyFileException
175
	 * @throws VerificationFailedException
176
	 * @throws RequestException
177
	 * @throws ApiClientDriverException
178
	 * @throws InvalidSignatureException
179
	 */
180 15
	private function request(
181
		HttpMethod $method,
182
		string $url,
183
		array $queries,
184
		?array $data,
185
		SignatureDataFormatter $responseSignatureDataFormatter,
186
		?Closure $responseValidityCallback,
187
		array $extensions = []
188
	): Response
189
	{
190 15
		$urlFirstQueryPosition = strpos($url, '{');
191 15
		$endpointName = $urlFirstQueryPosition !== false ? substr($url, 0, $urlFirstQueryPosition - 1) : $url;
192 15
		$originalQueries = $queries;
193
194 15
		foreach ($queries as $key => $value) {
195 13
			if (strpos($url, '{' . $key . '}') === false) {
196
				continue;
197
			}
198
199 13
			$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
200 13
			unset($queries[$key]);
201
		}
202
203 15
		if ($queries !== []) {
204
			throw new InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
205
		}
206
207 15
		$requestStartTime = microtime(true);
208
209 15
		$response = $this->driver->request(
210 15
			$method,
211 15
			$this->apiUrl . '/' . $url,
212
			$data
213
		);
214
215 15
		$this->logRequest($method, $endpointName, $originalQueries, $data, $response, microtime(true) - $requestStartTime);
216
217 15
		if ($responseValidityCallback !== null) {
218
			$responseValidityCallback($response);
219
		}
220
221 15
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
222 7
			$decodedExtensions = [];
223 7
			if ($extensions !== [] && array_key_exists('extensions', (array) $response->getData())) {
224 1
				foreach ($response->getData()['extensions'] as $extensionData) {
225 1
					$name = $extensionData['extension'];
226 1
					if (!isset($extensions[$name])) {
227
						continue;
228
					}
229
230 1
					$handler = $extensions[$name];
231 1
					$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
232
				}
233
			}
234 7
			$responseData = $this->decodeData($response->getData() ?? [], $responseSignatureDataFormatter);
235 5
			unset($responseData['extensions']);
236
237 5
			return new Response(
238 5
				$response->getResponseCode(),
239
				$responseData,
240 5
				$response->getHeaders(),
241
				$decodedExtensions
242
			);
243
244
		}
245 8
		if ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
246 1
			return new Response(
247 1
				$response->getResponseCode(),
248 1
				null,
249 1
				$response->getHeaders()
250
			);
251
252
		}
253 7
		if ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
254 1
			throw new BadRequestException($response);
255
		}
256 6
		if ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
257 1
			throw new ForbiddenException($response);
258
		}
259 5
		if ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
260 1
			throw new NotFoundException($response);
261
		}
262 4
		if ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
263 1
			throw new MethodNotAllowedException($response);
264
		}
265 3
		if ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
266 1
			throw new TooManyRequestsException($response);
267
		}
268 2
		if ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
269 1
			throw new ServiceUnavailableException($response);
270
		}
271
272 1
		throw new InternalErrorException($response);
273
	}
274
275
	/**
276
	 * @param mixed[] $data
277
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
278
	 * @return Response
279
	 *
280
	 * @throws InvalidSignatureException
281
	 * @throws PrivateKeyFileException
282
	 * @throws SigningFailedException
283
	 * @throws PublicKeyFileException
284
	 * @throws VerificationFailedException
285
	 */
286 1
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
287
	{
288 1
		$response = new Response(
289 1
			ResponseCode::get(ResponseCode::S200_OK),
290
			$data
291
		);
292
293 1
		$this->logRequest(HttpMethod::get(HttpMethod::GET), 'payment/response', [], [], $response, 0.0);
294
295 1
		return new Response(
296 1
			$response->getResponseCode(),
297 1
			$this->decodeData($data, $responseSignatureDataFormatter),
298 1
			$response->getHeaders()
299
		);
300
	}
301
302
	/**
303
	 * @param mixed[] $data
304
	 * @param SignatureDataFormatter $signatureDataFormatter
305
	 * @return mixed[]
306
	 *
307
	 * @throws PrivateKeyFileException
308
	 * @throws SigningFailedException
309
	 */
310 15
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
311
	{
312 15
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
313 15
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
314
315 15
		return $data;
316
	}
317
318
	/**
319
	 * @param mixed[] $responseData
320
	 * @param SignatureDataFormatter $signatureDataFormatter
321
	 * @return mixed[]
322
	 *
323
	 * @throws InvalidSignatureException
324
	 * @throws PublicKeyFileException
325
	 * @throws VerificationFailedException
326
	 */
327 8
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
328
	{
329 8
		if (!array_key_exists('signature', $responseData)) {
330 1
			throw new InvalidSignatureException($responseData);
331
		}
332
333 7
		$signature = $responseData['signature'];
334 7
		unset($responseData['signature']);
335
336 7
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
337 1
			throw new InvalidSignatureException($responseData);
338
		}
339
340 6
		return $responseData;
341
	}
342
343
	/**
344
	 * @param HttpMethod $method
345
	 * @param string $url
346
	 * @param mixed[] $queries
347
	 * @param mixed[]|null $requestData
348
	 * @param Response $response
349
	 * @param float $responseTime
350
	 */
351 16
	private function logRequest(HttpMethod $method, string $url, array $queries, ?array $requestData, Response $response, float $responseTime): void
352
	{
353 16
		if ($this->logger === null) {
354 11
			return;
355
		}
356
357 5
		$responseData = $response->getData();
358
359 5
		unset($requestData['signature']);
360 5
		unset($queries['signature']);
361 5
		unset($responseData['signature']);
362
363 5
		if (isset($responseData['extensions'])) {
364
			foreach ($responseData['extensions'] as $key => $extensionData) {
365
				unset($responseData['extensions'][$key]['signature']);
366
			}
367
		}
368
		$context = [
369
			'request' => [
370 5
				'method' => $method->getValue(),
371 5
				'queries' => $queries,
372 5
				'data' => $requestData,
373
			],
374
			'response' => [
375 5
				'code' => $response->getResponseCode()->getValue(),
376 5
				'data' => $responseData,
377 5
				'headers' => $response->getHeaders(),
378 5
				'time' => $responseTime,
379
			],
380
		];
381
382 5
		$this->logger->info($url, $context);
383 5
	}
384
385
}
386