Issues (20)

src/Dispatching/Http/ApiClient.php (6 issues)

Labels
Severity
1
<?php
2
3
/**
4
 * This file is part of RussianPost SDK package.
5
 *
6
 * © Appwilio (http://appwilio.com), JhaoDa (https://github.com/jhaoda)
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Appwilio\RussianPostSDK\Dispatching\Http;
15
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\LoggerAwareTrait;
18
use Psr\Log\LoggerAwareInterface;
19
use GuzzleHttp\Psr7\Request;
20
use GuzzleHttp\ClientInterface;
21
use GuzzleHttp\Psr7\UploadedFile;
22
use GuzzleHttp\Exception\ClientException;
23
use GuzzleHttp\Exception\ServerException;
24
use Psr\Http\Message\RequestInterface;
25
use Psr\Http\Message\ResponseInterface;
26
use Appwilio\RussianPostSDK\Core\Arrayable;
27
use Appwilio\RussianPostSDK\Dispatching\Instantiator;
28
use Appwilio\RussianPostSDK\Dispatching\Exceptions\BadRequest;
29
use Appwilio\RussianPostSDK\Dispatching\Exceptions\ServerFault;
30
use Appwilio\RussianPostSDK\Dispatching\Contracts\DispatchingException;
31
use function GuzzleHttp\json_encode as guzzle_json_encode;
32
use function GuzzleHttp\json_decode as guzzle_json_decode;
33
use function GuzzleHttp\Psr7\stream_for as guzzle_stream_for;
34
use function GuzzleHttp\Psr7\build_query as guzzle_build_query;
35
use function GuzzleHttp\Psr7\modify_request as guzzle_modify_request;
36
37
final class ApiClient implements LoggerAwareInterface
38
{
39
    use LoggerAwareTrait;
40
41
    private const API_URL = 'https://otpravka-api.pochta.ru';
42
43
    /** @var Authentication */
44
    private $authentication;
45
46
    /** @var ClientInterface */
47
    private $httpClient;
48
49 34
    public function __construct(Authentication $authentication, ClientInterface $httpClient, LoggerInterface $logger)
50
    {
51 34
        $this->authentication = $authentication;
52 34
        $this->httpClient = $httpClient;
53 34
        $this->logger = $logger;
54 34
    }
55
56 23
    public function get(string $path, ?Arrayable $request = null, $type = null)
57
    {
58 23
        return $this->send('GET', ...\func_get_args());
0 ignored issues
show
func_get_args() is expanded, but the parameter $path of Appwilio\RussianPostSDK\...\Http\ApiClient::send() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
        return $this->send('GET', /** @scrutinizer ignore-type */ ...\func_get_args());
Loading history...
59
    }
60
61 6
    public function post(string $path, Arrayable $request, $type = null)
62
    {
63 6
        return $this->send('POST', ...\func_get_args());
0 ignored issues
show
func_get_args() is expanded, but the parameter $path of Appwilio\RussianPostSDK\...\Http\ApiClient::send() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

63
        return $this->send('POST', /** @scrutinizer ignore-type */ ...\func_get_args());
Loading history...
64
    }
65
66 1
    public function put(string $path, Arrayable $request, $type = null)
67
    {
68 1
        return $this->send('PUT', ...\func_get_args());
0 ignored issues
show
func_get_args() is expanded, but the parameter $path of Appwilio\RussianPostSDK\...\Http\ApiClient::send() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
        return $this->send('PUT', /** @scrutinizer ignore-type */ ...\func_get_args());
Loading history...
69
    }
70
71 1
    public function delete(string $path, Arrayable $request, $type = null)
72
    {
73 1
        return $this->send('DELETE', ...\func_get_args());
0 ignored issues
show
func_get_args() is expanded, but the parameter $path of Appwilio\RussianPostSDK\...\Http\ApiClient::send() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

73
        return $this->send('DELETE', /** @scrutinizer ignore-type */ ...\func_get_args());
Loading history...
74
    }
75
76
    /**
77
     * Выполнение запроса.
78
     *
79
     * @param  string          $method
80
     * @param  string          $path
81
     * @param  Arrayable|null  $request
82
     * @param  mixed           $responseType
83
     *
84
     * @throws DispatchingException
85
     *
86
     * @return mixed
87
     */
88 31
    private function send(string $method, string $path, ?Arrayable $request = null, $responseType = null)
89
    {
90 31
        $method = \strtoupper($method);
91
92
        try {
93 31
            $response = $this->httpClient->send($this->buildHttpRequest($method, $path, $request));
94
95 12
            $contentType = $response->getHeaderLine('Content-Type');
96
97 12
            if (\preg_match('~^application/(pdf|zip)$~', $contentType, $matches)) {
98 1
                return $this->buildFile($response, $matches[1]);
99
            }
100
101 11
            if (\preg_match('~^application/json~', $contentType)) {
102 10
                $content = $this->getResponseContent($response);
103
104 10
                $this->logger->info("Dispatching response: status={$response->getStatusCode()}", $content);
105
106 10
                return $responseType === null
107 4
                    ? $content
108 10
                    : Instantiator::instantiate($responseType, $content);
109
            }
110
111 1
            throw new BadRequest();
112 20
        } catch (ClientException $e) {
113 18
            throw $this->handleClientException($e);
114 2
        } catch (ServerException $e) {
115 1
            throw $this->handleServerException($e);
116 1
        } catch (\Exception $e) {
117 1
            $this->logException($e->getCode(), $e->getMessage());
118
119 1
            throw $e;
120
        }
121
    }
122
123 31
    private function buildHttpRequest(string $method, string $path, ?Arrayable $payload): RequestInterface
124
    {
125 31
        $request = $this->authentication->authenticate(
126 31
            new Request($method, self::API_URL.$path, ['Accept' => 'application/json;charset=UTF-8'])
127
        );
128
129 31
        if ($payload === null) {
130 23
            $this->logger->info("Dispatching request: {$path}");
131
132 23
            return $request;
133
        }
134
135 9
        $data = $this->serializeRequestData(\array_filter($payload->toArray()));
136
137 9
        $this->logger->info("Dispatching request: {$path}", $data);
138
139 9
        if ($method === 'GET') {
140 1
            return guzzle_modify_request($request, ['query' => guzzle_build_query($data)]);
141
        }
142
143
        return $request
144 8
            ->withHeader('Content-Type', 'application/json;charset=UTF-8')
145 8
            ->withBody(guzzle_stream_for(guzzle_json_encode($data)));
146
    }
147
148 9
    private function serializeRequestData(array $data): array
149
    {
150
        return \array_map(function ($value) {
151 9
            if (\is_object($value) && $value instanceof \JsonSerializable) {
152 1
                return $value->jsonSerialize();
153
            }
154
155 9
            if (\is_array($value)) {
156 5
                return $this->serializeRequestData($value);
157
            }
158
159 9
            return $value;
160 9
        }, $data);
161
    }
162
163 1
    private function buildFile(ResponseInterface $response, string $type): UploadedFile
164
    {
165 1
        \preg_match('~=(.+)$~', $response->getHeaderLine('Content-Disposition'), $matches);
166
167 1
        $fileName = "{$matches[1]}.{$type}";
168 1
        $fileSize = $response->getBody()->getSize();
169
170 1
        $this->logger->info(\vsprintf('Dispatching response: status=%s, file=%s, size=%s bytes', [
171 1
            $response->getStatusCode(),
172 1
            $fileName,
173 1
            $fileSize,
174
        ]));
175
176 1
        return new UploadedFile(
177 1
            $response->getBody(), $fileSize, \UPLOAD_ERR_OK, $fileName, $response->getHeaderLine('Content-Type')
178
        );
179
    }
180
181 18
    private function handleClientException(ClientException $exception): BadRequest
182
    {
183 18
        if (\in_array($exception->getCode(), [401, 403])) {
184 12
            throw $this->handleAuthenticationException($exception);
185
        }
186
187 6
        $content = $this->getResponseContent($exception->getResponse());
0 ignored issues
show
It seems like $exception->getResponse() can also be of type null; however, parameter $response of Appwilio\RussianPostSDK\...t::getResponseContent() does only seem to accept Psr\Http\Message\ResponseInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
        $content = $this->getResponseContent(/** @scrutinizer ignore-type */ $exception->getResponse());
Loading history...
188
189 6
        $this->logException($exception->getCode(), $exception->getMessage(), $content);
190
191 6
        return new BadRequest(
192 6
            $content['message'] ?? $content['error'] ?? $content['desc'],
193 6
            (int) ($content['status'] ?? $content['code'] ?? $exception->getCode())
194
        );
195
    }
196
197 12
    private function handleAuthenticationException(ClientException $exception): BadRequest
198
    {
199 12
        $content = $this->getResponseContent($exception->getResponse());
0 ignored issues
show
It seems like $exception->getResponse() can also be of type null; however, parameter $response of Appwilio\RussianPostSDK\...t::getResponseContent() does only seem to accept Psr\Http\Message\ResponseInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

199
        $content = $this->getResponseContent(/** @scrutinizer ignore-type */ $exception->getResponse());
Loading history...
200
201 12
        $this->logException($exception->getCode(), $exception->getMessage(), $content);
202
203 12
        return new BadRequest(
204 12
            $content['message'] ?? $content['desc'] ?? '',
205 12
            isset($content['code']) ? (int) $content['code'] : $exception->getCode(),
206
            $exception
207
        );
208
    }
209
210 1
    private function handleServerException(ServerException $exception): ServerFault
211
    {
212 1
        return new ServerFault($exception->getMessage(), $exception->getCode(), $exception);
213
    }
214
215 28
    private function getResponseContent(ResponseInterface $response): array
216
    {
217 28
        return guzzle_json_decode((string) $response->getBody(), true);
218
    }
219
220 19
    private function logException(int $status, string $message, array $data = []): void
221
    {
222 19
        $this->logger->error("code={$status}, {$message}", $data);
223 19
    }
224
}
225