1: <?php
2: namespace Worldline\Acquiring\Sdk;
3:
4: use Exception;
5: use UnexpectedValueException;
6: use Worldline\Acquiring\Sdk\Authentication\Authenticator;
7: use Worldline\Acquiring\Sdk\Communication\Connection;
8: use Worldline\Acquiring\Sdk\Communication\ConnectionResponse;
9: use Worldline\Acquiring\Sdk\Communication\DefaultConnection;
10: use Worldline\Acquiring\Sdk\Communication\ErrorResponseException;
11: use Worldline\Acquiring\Sdk\Communication\MetadataProvider;
12: use Worldline\Acquiring\Sdk\Communication\MultipartDataObject;
13: use Worldline\Acquiring\Sdk\Communication\MultipartFormDataObject;
14: use Worldline\Acquiring\Sdk\Communication\RequestObject;
15: use Worldline\Acquiring\Sdk\Communication\ResponseBuilder;
16: use Worldline\Acquiring\Sdk\Communication\ResponseClassMap;
17: use Worldline\Acquiring\Sdk\Communication\ResponseFactory;
18: use Worldline\Acquiring\Sdk\Communication\UuidGenerator;
19: use Worldline\Acquiring\Sdk\Domain\DataObject;
20: use Worldline\Acquiring\Sdk\Logging\CommunicatorLogger;
21:
22: /**
23: * Class Communicator
24: *
25: * @package Worldline\Acquiring\Sdk
26: */
27: class Communicator
28: {
29: const MIME_APPLICATION_JSON = 'application/json';
30:
31: /** @var string */
32: private string $apiEndpoint;
33:
34: /** @var Authenticator */
35: private Authenticator $authenticator;
36:
37: /** @var Connection */
38: private Connection $connection;
39:
40: /** @var MetadataProvider */
41: private MetadataProvider $metadataProvider;
42:
43: /** @var ResponseFactory|null */
44: private ?ResponseFactory $responseFactory = null;
45:
46: /**
47: * @param CommunicatorConfiguration $communicatorConfiguration
48: * @param Authenticator $authenticator
49: * @param Connection|null $connection
50: */
51: public function __construct(
52: CommunicatorConfiguration $communicatorConfiguration,
53: Authenticator $authenticator,
54: Connection $connection = null
55: )
56: {
57: $this->apiEndpoint = $communicatorConfiguration->getApiEndpoint();
58: $this->authenticator = $authenticator;
59: $this->connection = $connection != null ? $connection : new DefaultConnection($communicatorConfiguration);
60: $this->metadataProvider = new MetadataProvider($communicatorConfiguration);
61: }
62:
63: /**
64: * @param CommunicatorLogger $communicatorLogger
65: */
66: public function enableLogging(CommunicatorLogger $communicatorLogger): void
67: {
68: $this->connection->enableLogging($communicatorLogger);
69: }
70:
71: /**
72: *
73: */
74: public function disableLogging(): void
75: {
76: $this->connection->disableLogging();
77: }
78:
79: /**
80: * @param ResponseClassMap $responseClassMap
81: * @param string $relativeUriPath
82: * @param RequestObject|null $requestParameters
83: * @param CallContext|null $callContext
84: * @return DataObject
85: * @throws Exception
86: */
87: public function get(
88: ResponseClassMap $responseClassMap,
89: string $relativeUriPath,
90: RequestObject $requestParameters = null,
91: CallContext $callContext = null
92: ): ?DataObject
93: {
94: $relativeUriPathWithRequestParameters =
95: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
96: $requestHeaders =
97: $this->getRequestHeaders('GET', $relativeUriPathWithRequestParameters, null, $callContext);
98:
99: $responseBuilder = new ResponseBuilder();
100: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder) {
101: $responseBuilder->setHttpStatusCode($httpStatusCode);
102: $responseBuilder->setHeaders($headers);
103: $responseBuilder->appendBody($data);
104: };
105:
106: $this->connection->get(
107: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
108: $requestHeaders,
109: $responseHandler
110: );
111: $connectionResponse = $responseBuilder->getResponse();
112: $this->updateCallContext($connectionResponse, $callContext);
113: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
114: $httpStatusCode = $connectionResponse->getHttpStatusCode();
115: if ($httpStatusCode >= 400) {
116: throw new ErrorResponseException($httpStatusCode, $response);
117: }
118: return $response;
119: }
120:
121: /**
122: * @param callable $bodyHandler Callable accepting a response body chunk and the response headers
123: * @param ResponseClassMap $responseClassMap Used for error handling
124: * @param string $relativeUriPath
125: * @param RequestObject|null $requestParameters
126: * @param CallContext|null $callContext
127: * @throws Exception
128: */
129: public function getWithBinaryResponse(
130: callable $bodyHandler,
131: ResponseClassMap $responseClassMap,
132: string $relativeUriPath,
133: RequestObject $requestParameters = null,
134: CallContext $callContext = null
135: ): void
136: {
137: $relativeUriPathWithRequestParameters =
138: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
139: $requestHeaders =
140: $this->getRequestHeaders('GET', $relativeUriPathWithRequestParameters, null, $callContext);
141:
142: $responseBuilder = new ResponseBuilder();
143: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder, $bodyHandler) {
144: $responseBuilder->setHttpStatusCode($httpStatusCode);
145: $responseBuilder->setHeaders($headers);
146: if ($httpStatusCode >= 400) {
147: $responseBuilder->appendBody($data);
148: } else {
149: call_user_func($bodyHandler, $data, $headers);
150: }
151: };
152:
153: $this->connection->get(
154: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
155: $requestHeaders,
156: $responseHandler
157: );
158: $connectionResponse = $responseBuilder->getResponse();
159: $this->updateCallContext($connectionResponse, $callContext);
160: $httpStatusCode = $connectionResponse->getHttpStatusCode();
161: if ($httpStatusCode >= 400) {
162: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
163: throw new ErrorResponseException($httpStatusCode, $response);
164: }
165: }
166:
167: /**
168: * @param ResponseClassMap $responseClassMap
169: * @param string $relativeUriPath
170: * @param RequestObject|null $requestParameters
171: * @param CallContext|null $callContext
172: * @return DataObject
173: * @throws Exception
174: */
175: public function delete(
176: ResponseClassMap $responseClassMap,
177: string $relativeUriPath,
178: RequestObject $requestParameters = null,
179: CallContext $callContext = null
180: ): ?DataObject
181: {
182: $relativeUriPathWithRequestParameters =
183: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
184: $requestHeaders =
185: $this->getRequestHeaders('DELETE', $relativeUriPathWithRequestParameters, null, $callContext);
186:
187: $responseBuilder = new ResponseBuilder();
188: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder) {
189: $responseBuilder->setHttpStatusCode($httpStatusCode);
190: $responseBuilder->setHeaders($headers);
191: $responseBuilder->appendBody($data);
192: };
193:
194: $this->connection->delete(
195: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
196: $requestHeaders,
197: $responseHandler
198: );
199: $connectionResponse = $responseBuilder->getResponse();
200: $this->updateCallContext($connectionResponse, $callContext);
201: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
202: $httpStatusCode = $connectionResponse->getHttpStatusCode();
203: if ($httpStatusCode >= 400) {
204: throw new ErrorResponseException($httpStatusCode, $response);
205: }
206: return $response;
207: }
208:
209: /**
210: * @param callable $bodyHandler Callable accepting a response body chunk and the response headers
211: * @param ResponseClassMap $responseClassMap Used for error handling
212: * @param string $relativeUriPath
213: * @param RequestObject|null $requestParameters
214: * @param CallContext|null $callContext
215: * @throws Exception
216: */
217: public function deleteWithBinaryResponse(
218: callable $bodyHandler,
219: ResponseClassMap $responseClassMap,
220: string $relativeUriPath,
221: RequestObject $requestParameters = null,
222: CallContext $callContext = null
223: ): void
224: {
225: $relativeUriPathWithRequestParameters =
226: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
227: $requestHeaders =
228: $this->getRequestHeaders('DELETE', $relativeUriPathWithRequestParameters, null, $callContext);
229:
230: $responseBuilder = new ResponseBuilder();
231: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder, $bodyHandler) {
232: $responseBuilder->setHttpStatusCode($httpStatusCode);
233: $responseBuilder->setHeaders($headers);
234: if ($httpStatusCode >= 400) {
235: $responseBuilder->appendBody($data);
236: } else {
237: call_user_func($bodyHandler, $data, $headers);
238: }
239: };
240:
241: $this->connection->delete(
242: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
243: $requestHeaders,
244: $responseHandler
245: );
246: $connectionResponse = $responseBuilder->getResponse();
247: $this->updateCallContext($connectionResponse, $callContext);
248: $httpStatusCode = $connectionResponse->getHttpStatusCode();
249: if ($httpStatusCode >= 400) {
250: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
251: throw new ErrorResponseException($httpStatusCode, $response);
252: }
253: }
254:
255: /**
256: * @param ResponseClassMap $responseClassMap
257: * @param string $relativeUriPath
258: * @param DataObject|MultipartDataObject|MultipartFormDataObject|null $requestBodyObject
259: * @param RequestObject|null $requestParameters
260: * @param CallContext|null $callContext
261: * @return DataObject
262: * @throws Exception
263: */
264: public function post(
265: ResponseClassMap $responseClassMap,
266: string $relativeUriPath,
267: $requestBodyObject = null,
268: RequestObject $requestParameters = null,
269: CallContext $callContext = null
270: ): ?DataObject
271: {
272: $relativeUriPathWithRequestParameters =
273: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
274: if ($requestBodyObject instanceof MultipartFormDataObject) {
275: $contentType = $requestBodyObject->getContentType();
276: $requestBody = $requestBodyObject;
277: } elseif ($requestBodyObject instanceof MultipartDataObject) {
278: $multipart = $requestBodyObject->toMultipartFormDataObject();
279: $contentType = $multipart->getContentType();
280: $requestBody = $multipart;
281: } elseif ($requestBodyObject instanceof DataObject || is_null($requestBodyObject)) {
282: $contentType = static::MIME_APPLICATION_JSON;
283: $requestBody = $requestBodyObject ? $requestBodyObject->toJson() : '';
284: } else {
285: throw new UnexpectedValueException('Unsupported request body');
286: }
287: $requestHeaders =
288: $this->getRequestHeaders('POST', $relativeUriPathWithRequestParameters, $contentType, $callContext);
289:
290: $responseBuilder = new ResponseBuilder();
291: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder) {
292: $responseBuilder->setHttpStatusCode($httpStatusCode);
293: $responseBuilder->setHeaders($headers);
294: $responseBuilder->appendBody($data);
295: };
296:
297: $this->connection->post(
298: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
299: $requestHeaders,
300: $requestBody,
301: $responseHandler
302: );
303: $connectionResponse = $responseBuilder->getResponse();
304: $this->updateCallContext($connectionResponse, $callContext);
305: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
306: $httpStatusCode = $connectionResponse->getHttpStatusCode();
307: if ($httpStatusCode >= 400) {
308: throw new ErrorResponseException($httpStatusCode, $response);
309: }
310: return $response;
311: }
312:
313: /**
314: * @param callable $bodyHandler Callable accepting a response body chunk and the response headers
315: * @param ResponseClassMap $responseClassMap Used for error handling
316: * @param string $relativeUriPath
317: * @param DataObject|MultipartDataObject|MultipartFormDataObject|null $requestBodyObject
318: * @param RequestObject|null $requestParameters
319: * @param CallContext|null $callContext
320: * @throws Exception
321: */
322: public function postWithBinaryResponse(
323: callable $bodyHandler,
324: ResponseClassMap $responseClassMap,
325: string $relativeUriPath,
326: $requestBodyObject = null,
327: RequestObject $requestParameters = null,
328: CallContext $callContext = null
329: ): void
330: {
331: $relativeUriPathWithRequestParameters =
332: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
333: if ($requestBodyObject instanceof MultipartFormDataObject) {
334: $contentType = $requestBodyObject->getContentType();
335: $requestBody = $requestBodyObject;
336: } elseif ($requestBodyObject instanceof MultipartDataObject) {
337: $multipart = $requestBodyObject->toMultipartFormDataObject();
338: $contentType = $multipart->getContentType();
339: $requestBody = $multipart;
340: } elseif ($requestBodyObject instanceof DataObject || is_null($requestBodyObject)) {
341: $contentType = static::MIME_APPLICATION_JSON;
342: $requestBody = $requestBodyObject ? $requestBodyObject->toJson() : '';
343: } else {
344: throw new UnexpectedValueException('Unsupported request body');
345: }
346: $requestHeaders =
347: $this->getRequestHeaders('POST', $relativeUriPathWithRequestParameters, $contentType, $callContext);
348:
349: $responseBuilder = new ResponseBuilder();
350: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder, $bodyHandler) {
351: $responseBuilder->setHttpStatusCode($httpStatusCode);
352: $responseBuilder->setHeaders($headers);
353: if ($httpStatusCode >= 400) {
354: $responseBuilder->appendBody($data);
355: } else {
356: call_user_func($bodyHandler, $data, $headers);
357: }
358: };
359:
360: $this->connection->post(
361: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
362: $requestHeaders,
363: $requestBody,
364: $responseHandler
365: );
366: $connectionResponse = $responseBuilder->getResponse();
367: $this->updateCallContext($connectionResponse, $callContext);
368: $httpStatusCode = $connectionResponse->getHttpStatusCode();
369: if ($httpStatusCode >= 400) {
370: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
371: throw new ErrorResponseException($httpStatusCode, $response);
372: }
373: }
374:
375: /**
376: * @param ResponseClassMap $responseClassMap
377: * @param string $relativeUriPath
378: * @param DataObject|MultipartDataObject|MultipartFormDataObject|null $requestBodyObject
379: * @param RequestObject|null $requestParameters
380: * @param CallContext|null $callContext
381: * @return DataObject
382: * @throws Exception
383: */
384: public function put(
385: ResponseClassMap $responseClassMap,
386: string $relativeUriPath,
387: $requestBodyObject = null,
388: RequestObject $requestParameters = null,
389: CallContext $callContext = null
390: ): ?DataObject
391: {
392: $relativeUriPathWithRequestParameters =
393: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
394: if ($requestBodyObject instanceof MultipartFormDataObject) {
395: $contentType = $requestBodyObject->getContentType();
396: $requestBody = $requestBodyObject;
397: } elseif ($requestBodyObject instanceof MultipartDataObject) {
398: $multipart = $requestBodyObject->toMultipartFormDataObject();
399: $contentType = $multipart->getContentType();
400: $requestBody = $multipart;
401: } elseif ($requestBodyObject instanceof DataObject || is_null($requestBodyObject)) {
402: $contentType = static::MIME_APPLICATION_JSON;
403: $requestBody = $requestBodyObject ? $requestBodyObject->toJson() : '';
404: } else {
405: throw new UnexpectedValueException('Unsupported request body');
406: }
407: $requestHeaders =
408: $this->getRequestHeaders('PUT', $relativeUriPathWithRequestParameters, $contentType, $callContext);
409:
410: $responseBuilder = new ResponseBuilder();
411: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder) {
412: $responseBuilder->setHttpStatusCode($httpStatusCode);
413: $responseBuilder->setHeaders($headers);
414: $responseBuilder->appendBody($data);
415: };
416:
417: $this->connection->put(
418: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
419: $requestHeaders,
420: $requestBody,
421: $responseHandler
422: );
423: $connectionResponse = $responseBuilder->getResponse();
424: $this->updateCallContext($connectionResponse, $callContext);
425: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
426: $httpStatusCode = $connectionResponse->getHttpStatusCode();
427: if ($httpStatusCode >= 400) {
428: throw new ErrorResponseException($httpStatusCode, $response);
429: }
430: return $response;
431: }
432:
433: /**
434: * @param callable $bodyHandler Callable accepting a response body chunk and the response headers
435: * @param ResponseClassMap $responseClassMap Used for error handling
436: * @param string $relativeUriPath
437: * @param DataObject|MultipartDataObject|MultipartFormDataObject|null $requestBodyObject
438: * @param RequestObject|null $requestParameters
439: * @param CallContext|null $callContext
440: * @throws Exception
441: */
442: public function putWithBinaryResponse(
443: callable $bodyHandler,
444: ResponseClassMap $responseClassMap,
445: string $relativeUriPath,
446: $requestBodyObject = null,
447: RequestObject $requestParameters = null,
448: CallContext $callContext = null
449: ): void
450: {
451: $relativeUriPathWithRequestParameters =
452: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
453: if ($requestBodyObject instanceof MultipartFormDataObject) {
454: $contentType = $requestBodyObject->getContentType();
455: $requestBody = $requestBodyObject;
456: } elseif ($requestBodyObject instanceof MultipartDataObject) {
457: $multipart = $requestBodyObject->toMultipartFormDataObject();
458: $contentType = $multipart->getContentType();
459: $requestBody = $multipart;
460: } elseif ($requestBodyObject instanceof DataObject || is_null($requestBodyObject)) {
461: $contentType = static::MIME_APPLICATION_JSON;
462: $requestBody = $requestBodyObject ? $requestBodyObject->toJson() : '';
463: } else {
464: throw new UnexpectedValueException('Unsupported request body');
465: }
466: $requestHeaders =
467: $this->getRequestHeaders('PUT', $relativeUriPathWithRequestParameters, $contentType, $callContext);
468:
469: $responseBuilder = new ResponseBuilder();
470: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder, $bodyHandler) {
471: $responseBuilder->setHttpStatusCode($httpStatusCode);
472: $responseBuilder->setHeaders($headers);
473: if ($httpStatusCode >= 400) {
474: $responseBuilder->appendBody($data);
475: } else {
476: call_user_func($bodyHandler, $data, $headers);
477: }
478: };
479:
480: $this->connection->put(
481: $this->apiEndpoint . $relativeUriPathWithRequestParameters,
482: $requestHeaders,
483: $requestBody,
484: $responseHandler
485: );
486: $connectionResponse = $responseBuilder->getResponse();
487: $this->updateCallContext($connectionResponse, $callContext);
488: $httpStatusCode = $connectionResponse->getHttpStatusCode();
489: if ($httpStatusCode >= 400) {
490: $response = $this->getResponseFactory()->createResponse($connectionResponse, $responseClassMap);
491: throw new ErrorResponseException($httpStatusCode, $response);
492: }
493: }
494:
495: /**
496: * @param ConnectionResponse $response
497: * @param CallContext|null $callContext
498: */
499: protected function updateCallContext(ConnectionResponse $response, CallContext $callContext = null): void
500: {
501: // no context specific headers at this time
502: }
503:
504: /**
505: * @param string $relativeUriPath
506: * @param RequestObject|null $requestParameters
507: * @return string
508: * @throws Exception
509: */
510: protected function getRequestUri(string $relativeUriPath, RequestObject $requestParameters = null): string
511: {
512: return
513: $this->apiEndpoint .
514: $this->getRelativeUriPathWithRequestParameters($relativeUriPath, $requestParameters);
515: }
516:
517: /**
518: * @param string $httpMethod
519: * @param string $relativeUriPathWithRequestParameters
520: * @param string|null $contentType
521: * @param CallContext|null $callContext
522: * @return string[]
523: */
524: protected function getRequestHeaders(
525: string $httpMethod,
526: string $relativeUriPathWithRequestParameters,
527: ?string $contentType,
528: CallContext $callContext = null
529: ): array
530: {
531: $rfc2616Date = self::getRfc161Date();
532: $requestHeaders = array();
533: if ($contentType) {
534: $requestHeaders['Content-Type'] = $contentType;
535: }
536: $requestHeaders['Date'] = $rfc2616Date;
537: $requestHeaders['X-WL-ServerMetaInfo'] = $this->metadataProvider->getServerMetaInfoValue();
538: // no context specific headers at this time
539: $requestHeaders['Trace-Id'] = UuidGenerator::generatedUuid();
540: $requestHeaders['Authorization'] = $this->authenticator->getAuthorization($httpMethod, $relativeUriPathWithRequestParameters, $requestHeaders);
541: return $requestHeaders;
542: }
543:
544: /**
545: * @return string
546: */
547: protected static function getRfc161Date(): string
548: {
549: return gmdate('D, d M Y H:i:s T');
550: }
551:
552: /**
553: * @param string $relativeUriPath
554: * @param RequestObject|null $requestParameters
555: * @return string
556: */
557: protected function getRelativeUriPathWithRequestParameters(
558: string $relativeUriPath,
559: RequestObject $requestParameters = null
560: ): string
561: {
562: if (is_null($requestParameters)) {
563: return $relativeUriPath;
564: }
565: $requestParameterObjectVars = get_object_vars($requestParameters);
566: if (count($requestParameterObjectVars) == 0) {
567: return $relativeUriPath;
568: }
569: $httpQuery = http_build_query($requestParameterObjectVars);
570: // remove [0], [1] etc. that are added if properties are arrays
571: $httpQuery = preg_replace('/%5B[0-9]+%5D/simU', '', $httpQuery);
572: return $relativeUriPath . '?' . $httpQuery;
573: }
574:
575: /** @return ResponseFactory */
576: protected function getResponseFactory(): ResponseFactory
577: {
578: if (is_null($this->responseFactory)) {
579: $this->responseFactory = new ResponseFactory();
580: }
581: return $this->responseFactory;
582: }
583: }
584: