Completed
Push — master ( be5157...f95229 )
by Nikita
02:46
created

Client::sendRequest()   C

Complexity

Conditions 8
Paths 36

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 19
nc 36
nop 4
1
<?php
2
declare(strict_types=1);
3
4
namespace Yproximite\Api\Client;
5
6
use Http\Client\HttpClient;
7
use Http\Message\MessageFactory;
8
use Psr\Http\Message\StreamInterface;
9
use Http\Client\Exception\HttpException;
10
use Http\Discovery\MessageFactoryDiscovery;
11
use Http\Client\Exception\TransferException as HttpTransferException;
12
13
use Yproximite\Api\Exception\LogicException;
14
use Yproximite\Api\Exception\TransferException;
15
use Yproximite\Api\Exception\AuthenficationException;
16
use Yproximite\Api\Exception\InvalidResponseException;
17
18
/**
19
 * Class Client
20
 */
21
class Client
22
{
23
    const BASE_URL = 'https://api.yproximite.fr';
24
25
    /**
26
     * @var string|null
27
     */
28
    private $baseUrl;
29
30
    /**
31
     * @var string|null
32
     */
33
    private $apiKey;
34
35
    /**
36
     * @var HttpClient
37
     */
38
    private $httpClient;
39
40
    /**
41
     * @var MessageFactory|null
42
     */
43
    private $messageFactory;
44
45
    /**
46
     * @var string
47
     */
48
    private $apiToken;
49
50
    /**
51
     * Used to determine if token was used. In some cases the token could be invalidated during the usage of the API.
52
     *
53
     * @var bool
54
     */
55
    private $apiTokenFresh = true;
56
57
    /**
58
     * Client constructor.
59
     *
60
     * @param HttpClient          $httpClient
61
     * @param string|null         $apiKey
62
     * @param string|null         $baseUrl
63
     * @param MessageFactory|null $messageFactory
64
     *
65
     * @throws LogicException
66
     */
67
    public function __construct(
68
        HttpClient $httpClient,
69
        string $apiKey = null,
70
        string $baseUrl = null,
71
        MessageFactory $messageFactory = null
72
    ) {
73
        if (empty($baseUrl)) {
74
            throw new LogicException('The base url cannot be empty.');
75
        }
76
77
        $this->httpClient     = $httpClient;
78
        $this->messageFactory = $messageFactory;
79
        $this->apiKey         = $apiKey;
80
        $this->baseUrl        = $baseUrl;
81
    }
82
83
    /**
84
     * @param null|string $baseUrl
85
     */
86
    public function setBaseUrl(string $baseUrl = null)
87
    {
88
        $this->baseUrl = $baseUrl;
89
    }
90
91
    /**
92
     * @param null|string $apiKey
93
     */
94
    public function setApiKey(string $apiKey = null)
95
    {
96
        $this->apiKey = $apiKey;
97
98
        $this->resetApiToken();
99
    }
100
101
    /**
102
     * Sends a request
103
     *
104
     * @param string                                     $method
105
     * @param string                                     $path
106
     * @param array|resource|string|StreamInterface|null $body
107
     * @param bool                                       $withAuthorization
108
     *
109
     * @return array|null
110
     * @throws InvalidResponseException
111
     */
112
    public function sendRequest(string $method, string $path, $body = null, bool $withAuthorization = true)
113
    {
114
        $uri  = $this->getSafeBaseUrl();
115
        $uri .= $path;
116
117
        $rawData = is_array($body) ? http_build_query($body) : $body;
118
        $body    = null;
119
120
        if (in_array($method, $this->getQueryMethods())) {
121
            if (is_string($rawData) && $rawData !== '') {
122
                $uri .= '?'.$rawData;
123
            }
124
        } else {
125
            $body = $rawData;
126
        }
127
128
        $content = $withAuthorization
129
            ? $this->sendRequestWithAuthorization($method, $uri, $body)
130
            : $this->doSendRequest($method, $uri, $body, false)
131
        ;
132
133
        if (empty($content)) {
134
            return null;
135
        }
136
137
        $data = json_decode($content, true);
138
139
        if (json_last_error() !== JSON_ERROR_NONE) {
140
            throw new InvalidResponseException(sprintf('Could not decode the response of "%s %s".', $method, $path));
141
        }
142
143
        return $data;
144
    }
145
146
    /**
147
     * @return string
148
     */
149
    private function getSafeBaseUrl(): string
150
    {
151
        return !is_null($this->baseUrl) ? $this->baseUrl : self::BASE_URL;
152
    }
153
154
    /**
155
     * Sends a request with authorization and tries to renew the api token in case of error of authentication.
156
     *
157
     * @param string                               $method
158
     * @param string                               $uri
159
     * @param resource|string|StreamInterface|null $body
160
     *
161
     * @return string
162
     */
163
    private function sendRequestWithAuthorization(string $method, string $uri, $body): string
164
    {
165
        try {
166
            $content = $this->doSendRequest($method, $uri, $body);
167
        } catch (TransferException $e) {
168
            if ($e->getResponse() && $e->getResponse()->getStatusCode() === 401 && !$this->apiTokenFresh) {
169
                $this->resetApiToken();
170
171
                $content = $this->doSendRequest($method, $uri, $body);
172
            } else {
173
                throw $e;
174
            }
175
        }
176
177
        return $content;
178
    }
179
180
    /**
181
     * @param string                               $method
182
     * @param string                               $uri
183
     * @param resource|string|StreamInterface|null $body
184
     * @param bool                                 $withAuthorization
185
     *
186
     * @return string
187
     */
188
    private function doSendRequest(string $method, string $uri, $body, bool $withAuthorization = true): string
189
    {
190
        $headers = [];
191
192
        if ($withAuthorization) {
193
            $headers['Authorization'] = $this->getAuthorizationHeader();
194
        }
195
196
        $request = $this->getMessageFactory()->createRequest($method, $uri, $headers, $body);
197
198
        try {
199
            $response = $this->getHttpClient()->sendRequest($request);
200
        } catch (HttpTransferException $e) {
201
            throw new TransferException(
202
                $e->getMessage(),
203
                $request,
204
                $e instanceof HttpException ? $e->getResponse() : null
205
            );
206
        }
207
208
        return (string) $response->getBody();
209
    }
210
211
    /**
212
     * @return HttpClient
213
     */
214
    private function getHttpClient(): HttpClient
215
    {
216
        return $this->httpClient;
217
    }
218
219
    /**
220
     * @return MessageFactory
221
     */
222
    private function getMessageFactory(): MessageFactory
223
    {
224
        if ($this->messageFactory === null) {
225
            $this->messageFactory = MessageFactoryDiscovery::find();
226
        }
227
228
        return $this->messageFactory;
229
    }
230
231
    /**
232
     * Returns all methods that uses query string to transfer a request data
233
     *
234
     * @return array
235
     */
236
    private function getQueryMethods(): array
237
    {
238
        return ['GET', 'HEAD', 'DELETE'];
239
    }
240
241
    /**
242
     * @return string
243
     * @throws LogicException
244
     */
245
    private function getApiToken(): string
246
    {
247
        if (!is_null($this->apiToken)) {
248
            $this->apiTokenFresh = false;
249
        } else {
250
            $this->updateApiToken();
251
        }
252
253
        return $this->apiToken;
254
    }
255
256
    private function updateApiToken()
257
    {
258
        if (is_null($this->apiKey)) {
259
            throw new LogicException('The api key cannot be empty.');
260
        }
261
262
        try {
263
            $data = $this->sendRequest('POST', '/login_check', ['api_key' => $this->apiKey], false);
264
        } catch (TransferException $e) {
265
            throw new AuthenficationException('Could not request a token.');
266
        }
267
268
        if (!is_array($data) || !array_key_exists('token', $data)) {
269
            throw new AuthenficationException('Could not retreive a token.');
270
        }
271
272
        $this->apiToken = (string) $data['token'];
273
    }
274
275
    private function resetApiToken()
276
    {
277
        $this->apiToken      = null;
278
        $this->apiTokenFresh = true;
279
    }
280
281
    /**
282
     * @return string
283
     */
284
    private function getAuthorizationHeader(): string
285
    {
286
        return sprintf('Bearer %s', $this->getApiToken());
287
    }
288
}
289