Completed
Push — master ( 069e15...978af9 )
by Jan
02:19
created

ApiClient   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 96.83%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 12
dl 0
loc 364
ccs 122
cts 126
cp 0.9683
rs 9.3999
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 4 1
A __construct() 0 10 1
A get() 0 19 1
A post() 0 18 1
A put() 0 18 1
D request() 0 88 19
A createResponseByData() 0 15 1
A prepareData() 0 7 1
A decodeData() 0 15 3
B logRequest() 0 33 4
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|null
35
	 */
36
	private $apiUrl;
37
38 16
	public function __construct(
39
		ApiClientDriver $driver,
40
		CryptoService $cryptoService,
41
		string $apiUrl
42
	)
43
	{
44 16
		$this->driver = $driver;
45 16
		$this->cryptoService = $cryptoService;
46 16
		$this->apiUrl = $apiUrl;
47 16
	}
48
49 5
	public function setLogger(?LoggerInterface $logger): void
50
	{
51 5
		$this->logger = $logger;
52 5
	}
53
54
	/**
55
	 * @param string $url
56
	 * @param mixed[] $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 13
	public function get(
72
		string $url,
73
		array $data = [],
74
		SignatureDataFormatter $requestSignatureDataFormatter,
75
		SignatureDataFormatter $responseSignatureDataFormatter,
76
		?\Closure $responseValidityCallback = null,
77
		array $extensions = []
78
	): Response
79
	{
80 13
		return $this->request(
81 13
			HttpMethod::get(HttpMethod::GET),
82 13
			$url,
83 13
			$this->prepareData($data, $requestSignatureDataFormatter),
84 13
			null,
85 13
			$responseSignatureDataFormatter,
86 13
			$responseValidityCallback,
87 13
			$extensions
88
		);
89
	}
90
91
	/**
92
	 * @param string $url
93
	 * @param mixed[] $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 1
	public function post(
108
		string $url,
109
		array $data = [],
110
		SignatureDataFormatter $requestSignatureDataFormatter,
111
		SignatureDataFormatter $responseSignatureDataFormatter,
112
		array $extensions = []
113
	): Response
114
	{
115 1
		return $this->request(
116 1
			HttpMethod::get(HttpMethod::POST),
117 1
			$url,
118 1
			[],
119 1
			$this->prepareData($data, $requestSignatureDataFormatter),
120 1
			$responseSignatureDataFormatter,
121 1
			null,
122 1
			$extensions
123
		);
124
	}
125
126
	/**
127
	 * @param string $url
128
	 * @param mixed[] $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 1
	public function put(
143
		string $url,
144
		array $data = [],
145
		SignatureDataFormatter $requestSignatureDataFormatter,
146
		SignatureDataFormatter $responseSignatureDataFormatter,
147
		array $extensions = []
148
	): Response
149
	{
150 1
		return $this->request(
151 1
			HttpMethod::get(HttpMethod::PUT),
152 1
			$url,
153 1
			[],
154 1
			$this->prepareData($data, $requestSignatureDataFormatter),
155 1
			$responseSignatureDataFormatter,
156 1
			null,
157 1
			$extensions
158
		);
159
	}
160
161
	/**
162
	 * @param HttpMethod $method
163
	 * @param string $url
164
	 * @param mixed[] $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 15
	private function request(
180
		HttpMethod $method,
181
		string $url,
182
		array $queries = [],
183
		?array $data,
184
		SignatureDataFormatter $responseSignatureDataFormatter,
185
		?\Closure $responseValidityCallback,
186
		array $extensions = []
187
	): Response
188
	{
189 15
		$urlFirstQueryPosition = strpos($url, '{');
190 15
		$endpointName = ($urlFirstQueryPosition !== false ? substr($url, 0, $urlFirstQueryPosition - 1) : $url);
191 15
		$originalQueries = $queries;
192
193 15
		foreach ($queries as $key => $value) {
194 13
			if (strpos($url, '{' . $key . '}') !== false) {
195 13
				$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
196 13
				unset($queries[$key]);
197
			}
198
		}
199
200 15
		if ($queries !== []) {
201
			throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
202
		}
203
204 15
		$requestStartTime = microtime(true);
205
206 15
		$response = $this->driver->request(
207 15
			$method,
208 15
			$this->apiUrl . '/' . $url,
209 15
			$data
210
		);
211
212 15
		$this->logRequest($method, $endpointName, $originalQueries, $data, $response, microtime(true) - $requestStartTime);
213
214 15
		if ($responseValidityCallback !== null) {
215
			$responseValidityCallback($response);
216
		}
217
218 15
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
219 7
			$decodedExtensions = [];
220 7
			if ($extensions !== [] && array_key_exists('extensions', $response->getData())) {
221 1
				foreach ($response->getData()['extensions'] as $extensionData) {
222 1
					$name = $extensionData['extension'];
223 1
					if (isset($extensions[$name])) {
224 1
						$handler = $extensions[$name];
225 1
						$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
226
					}
227
				}
228
			}
229 7
			$responseData = $this->decodeData($response->getData() ?: [], $responseSignatureDataFormatter);
230 5
			unset($responseData['extensions']);
231
232 5
			return new Response(
233 5
				$response->getResponseCode(),
234 5
				$responseData,
235 5
				$response->getHeaders(),
236 5
				$decodedExtensions
237
			);
238
239 8
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
240 1
			return new Response(
241 1
				$response->getResponseCode(),
242 1
				null,
243 1
				$response->getHeaders()
244
			);
245
246 7
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
247 1
			throw new BadRequestException($response);
248
249 6
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
250 1
			throw new ForbiddenException($response);
251
252 5
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
253 1
			throw new NotFoundException($response);
254
255 4
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
256 1
			throw new MethodNotAllowedException($response);
257
258 3
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
259 1
			throw new TooManyRequestsException($response);
260
261 2
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
262 1
			throw new ServiceUnavailableException($response);
263
		}
264
265 1
		throw new InternalErrorException($response);
266
	}
267
268
	/**
269
	 * @param mixed[] $data
270
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
271
	 * @return Response
272
	 *
273
	 * @throws InvalidSignatureException
274
	 * @throws PrivateKeyFileException
275
	 * @throws SigningFailedException
276
	 * @throws PublicKeyFileException
277
	 * @throws VerificationFailedException
278
	 */
279 1
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
280
	{
281 1
		$response = new Response(
282 1
			ResponseCode::get(ResponseCode::S200_OK),
283 1
			$data
284
		);
285
286 1
		$this->logRequest(HttpMethod::get(HttpMethod::GET), 'payment/response', [], [], $response, 0.0);
287
288 1
		return new Response(
289 1
			$response->getResponseCode(),
290 1
			$this->decodeData($data, $responseSignatureDataFormatter),
291 1
			$response->getHeaders()
292
		);
293
	}
294
295
	/**
296
	 * @param mixed[] $data
297
	 * @param SignatureDataFormatter $signatureDataFormatter
298
	 * @return mixed[]
299
	 *
300
	 * @throws PrivateKeyFileException
301
	 * @throws SigningFailedException
302
	 */
303 15
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
304
	{
305 15
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
306 15
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
307
308 15
		return $data;
309
	}
310
311
	/**
312
	 * @param mixed[] $responseData
313
	 * @param SignatureDataFormatter $signatureDataFormatter
314
	 * @return mixed[]
315
	 *
316
	 * @throws InvalidSignatureException
317
	 * @throws PublicKeyFileException
318
	 * @throws VerificationFailedException
319
	 */
320 8
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
321
	{
322 8
		if (!array_key_exists('signature', $responseData)) {
323 1
			throw new InvalidSignatureException($responseData);
324
		}
325
326 7
		$signature = $responseData['signature'];
327 7
		unset($responseData['signature']);
328
329 7
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
330 1
			throw new InvalidSignatureException($responseData);
331
		}
332
333 6
		return $responseData;
334
	}
335
336
	/**
337
	 * @param \SlevomatCsobGateway\Api\HttpMethod $method
338
	 * @param string $url
339
	 * @param mixed[] $queries
340
	 * @param mixed[]|null $requestData
341
	 * @param \SlevomatCsobGateway\Api\Response $response
342
	 * @param float $responseTime
343
	 */
344 16
	private function logRequest(HttpMethod $method, string $url, array $queries, ?array $requestData, Response $response, float $responseTime): void
345
	{
346 16
		if ($this->logger === null) {
347 11
			return;
348
		}
349
350 5
		$responseData = $response->getData();
351
352 5
		unset($requestData['signature']);
353 5
		unset($queries['signature']);
354 5
		unset($responseData['signature']);
355
356 5
		if (isset($responseData['extensions'])) {
357
			foreach ($responseData['extensions'] as $key => $extensionData) {
358
				unset($responseData['extensions'][$key]['signature']);
359
			}
360
		}
361
		$context = [
362
			'request' => [
363 5
				'method' => $method->getValue(),
364 5
				'queries' => $queries,
365 5
				'data' => $requestData,
366
			],
367
			'response' => [
368 5
				'code' => $response->getResponseCode()->getValue(),
369 5
				'data' => $responseData,
370 5
				'headers' => $response->getHeaders(),
371 5
				'time' => $responseTime,
372
			],
373
		];
374
375 5
		$this->logger->info($url, $context);
376 5
	}
377
378
}
379