Completed
Push — master ( 4968a9...664a15 )
by Anton
02:59
created

PublicAPIClient::prepareRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 2
nop 1
1
<?php
2
3
namespace Covery\Client;
4
5
use Covery\Client\Envelopes\ValidatorV1;
6
use Covery\Client\Requests\Decision;
7
use Covery\Client\Requests\Event;
8
use Covery\Client\Requests\Ping;
9
use GuzzleHttp\Psr7\Uri;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\NullLogger;
14
15
class PublicAPIClient
16
{
17
    /**
18
     * @var CredentialsInterface
19
     */
20
    private $credentials;
21
22
    /**
23
     * @var TransportInterface
24
     */
25
    private $transport;
26
27
    /**
28
     * @var LoggerInterface
29
     */
30
    private $logger;
31
32
    /**
33
     * @var ValidatorV1
34
     */
35
    private $validator;
36
37
    /**
38
     * @var string|null
39
     */
40
    private $url;
41
42
    /**
43
     * Client constructor.
44
     * @param CredentialsInterface $credentials
45
     * @param TransportInterface $transport
46
     * @param string|null $overrideURl
47
     * @param LoggerInterface|null $logger
48
     */
49
    public function __construct(
50
        CredentialsInterface $credentials,
51
        TransportInterface $transport,
52
        $overrideURl = null,
53
        LoggerInterface $logger = null
54
    ) {
55
        $this->credentials = $credentials;
56
        $this->transport = $transport;
57
        $this->logger = $logger === null ? new NullLogger() : $logger;
58
        $this->validator = new ValidatorV1();
59
        if ($overrideURl !== null) {
60
            if (substr($overrideURl, -1) != '/') {
61
                $overrideURl .= '/';
62
            }
63
64
            $this->url = $overrideURl;
65
        }
66
    }
67
68
    private function send(RequestInterface $request)
69
    {
70
        $request = $this->prepareRequest($request);
71
        try {
72
            $this->logger->info('Sending request to ' . $request->getUri());
73
            $before = microtime(true);
74
            $response = $this->transport->send($request);
75
            $this->logger->info(sprintf('Request done in %.2f', microtime(true) - $before));
76
        } catch (\Exception $inner) {
77
            $this->logger->error($inner->getMessage(), ['exception' => $inner]);
78
            // Wrapping exception
79
            throw new IoException('Error sending request', 0, $inner);
80
        }
81
        $code = $response->getStatusCode();
82
        $this->logger->debug('Received status code ' . $code);
83
84
        if ($code >= 400) {
85
            $this->handleNot200($response);
86
        }
87
88
        return $response->getBody()->getContents();
89
    }
90
91
    /**
92
     * Utility method, that prepares and signs request
93
     *
94
     * @param RequestInterface $request
95
     * @return RequestInterface
96
     */
97
    private function prepareRequest(RequestInterface $request)
98
    {
99
        $requestUrl = strval($request->getUri());
100
        if ($this->url !== null
101
            && substr($requestUrl, 0, strlen(TransportInterface::DEFAULT_URL)) === TransportInterface::DEFAULT_URL
102
        ) {
103
            // Replacing
104
            $request = $request->withUri(
105
                new Uri(str_replace(TransportInterface::DEFAULT_URL, $this->url, $requestUrl))
106
            );
107
        }
108
109
        return $this->credentials->signRequest($request);
110
    }
111
112
    /**
113
     * Utility function, that handles error response from Covery
114
     *
115
     * @param ResponseInterface $response
116
     * @throws Exception
117
     */
118
    private function handleNot200(ResponseInterface $response)
119
    {
120
        // Analyzing response
121
        if ($response->hasHeader('X-Maxwell-Status') && $response->hasHeader('X-Maxwell-Error-Message')) {
122
            // Extended data available
123
            $message = $response->getHeaderLine('X-Maxwell-Error-Message');
124
            $type = $response->getHeaderLine('X-Maxwell-Error-Type');
125
            if (strpos($type, 'AuthorizationRequiredException') !== false) {
126
                $this->logger->error('Authentication failure ' . $message);
127
                throw new AuthException($message, $response->getStatusCode());
128
            }
129
130
            switch ($message) {
131
                case 'Empty auth token':
132
                case 'Empty signature':
133
                case 'Empty nonce':
134
                    $this->logger->error('Authentication failure ' . $message);
135
                    throw new AuthException($message, $response->getStatusCode());
136
            }
137
138
            $this->logger->error('Covery error ' . $message);
139
            throw new DeliveredException($message, $response->getStatusCode());
140
        }
141
142
143
        throw new Exception("Communication failed with status code {$response->getStatusCode()}");
144
    }
145
146
    /**
147
     * Utility method, that reads JSON data
148
     *
149
     * @param $string
150
     * @return mixed|null
151
     * @throws Exception
152
     */
153
    private function readJson($string)
154
    {
155
        if (!is_string($string)) {
156
            throw new Exception("Unable to read JSON - not a string received");
157
        }
158
        if (strlen($string) === 0) {
159
            return null;
160
        }
161
162
        $data = json_decode($string, true);
163
        if ($data === null) {
164
            $message = 'Unable to decode JSON';
165
            if (function_exists('json_last_error_msg')) {
166
                $message = json_last_error_msg();
167
            }
168
169
            throw new Exception($message);
170
        }
171
172
        return $data;
173
    }
174
175
    /**
176
     * Sends request to Covery and returns access level, associated with
177
     * used credentials
178
     *
179
     * This method can be used for Covery health check and availability
180
     * On any problem (network, credentials, server side) this method
181
     * will throw an exception
182
     *
183
     * @return string
184
     * @throws Exception
185
     */
186
    public function ping()
187
    {
188
        $data = $this->readJson($this->send(new Ping()));
189
        if (!is_array($data) || !isset($data['level'])) {
190
            throw new Exception("Malformed response");
191
        }
192
193
        return $data['level'];
194
    }
195
196
    /**
197
     * Sends envelope to Covery and returns it's ID on Covery side
198
     * Before sending, validation is performed
199
     *
200
     * @param EnvelopeInterface $envelope
201
     * @return int
202
     * @throws Exception
203
     */
204
    public function sendEvent(EnvelopeInterface $envelope)
205
    {
206
        // Validating
207
        $this->validator->validate($envelope);
208
209
        // Sending
210
        $data = $this->readJson($this->send(new Event($envelope)));
211
212
        if (!is_array($data) || !isset($data['requestId']) || !is_int($data['requestId'])) {
213
            throw new Exception("Malformed response");
214
        }
215
216
        return $data['requestId'];
217
    }
218
219
    /**
220
     * Sends envelope to Covery for analysis
221
     *
222
     * @param EnvelopeInterface $envelope
223
     * @return Result
224
     * @throws Exception
225
     */
226
    public function makeDecision(EnvelopeInterface $envelope)
227
    {
228
        // Validating
229
        $this->validator->validate($envelope);
230
231
        // Sending
232
        $data = $this->readJson($this->send(new Decision($envelope)));
233
234
        if (!is_array($data)) {
235
            throw new Exception("Malformed response");
236
        }
237
238
        try {
239
            return new Result(
240
                $data['requestId'],
241
                $data['score'],
242
                $data['accept'],
243
                $data['reject'],
244
                $data['manual']
245
            );
246
        } catch (\Exception $error) {
247
            throw new Exception('Malformed response', 0, $error);
248
        }
249
    }
250
}
251