Passed
Pull Request — master (#7)
by Sandro
03:30
created

Client::sendRequest()   B

Complexity

Conditions 9
Paths 79

Size

Total Lines 59
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 9.4078

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 36
c 1
b 0
f 0
dl 0
loc 59
ccs 29
cts 35
cp 0.8286
rs 8.0555
cc 9
nc 79
nop 1
crap 9.4078

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Sandro Keil (https://sandro-keil.de)
4
 *
5
 * @link      http://github.com/sandrokeil/arangodb-php-client for the canonical source repository
6
 * @copyright Copyright (c) 2018-2019 Sandro Keil
7
 * @license   http://github.com/sandrokeil/arangodb-php-client/blob/master/LICENSE.md New BSD License
8
 */
9
10
declare(strict_types=1);
11
12
namespace ArangoDb;
13
14
use ArangoDb\Exception\ConnectionException;
15
use ArangoDb\Exception\NetworkException;
16
use ArangoDb\Exception\RequestFailedException;
17
use ArangoDb\Exception\TimeoutException;
18
use ArangoDb\Type\Collection;
19
use ArangoDb\Type\Document;
20
use ArangoDb\Type\GuardSupport;
21
use ArangoDb\Type\Type;
22
use Fig\Http\Message\StatusCodeInterface;
23
use Psr\Http\Client\ClientInterface;
24
use Psr\Http\Message\RequestFactoryInterface;
25
use Psr\Http\Message\RequestInterface;
26
use Psr\Http\Message\ResponseFactoryInterface;
27
use Psr\Http\Message\ResponseInterface;
28
use Psr\Http\Message\StreamFactoryInterface;
29
30
final class Client implements ClientInterface, SendTypeSupport
31
{
32
    /**
33
     * Chunk size in bytes
34
     */
35
    private const CHUNK_SIZE = 8192;
36
37
    /**
38
     * End of line mark used in HTTP
39
     */
40
    private const EOL = "\r\n";
41
42
    /**
43
     * Connection handle
44
     *
45
     * @var resource
46
     */
47
    private $handle;
48
49
    /**
50
     * @var bool
51
     */
52
    private $useKeepAlive;
53
54
    /**
55
     * @var ClientOptions
56
     */
57
    private $options;
58
59
    /**
60
     * @var string
61
     */
62
    private $baseUrl = '';
63
64
    /**
65
     * Default headers for all requests
66
     *
67
     * @var string
68
     */
69
    private $headerLines = '';
70
71
    /**
72
     * @var string
73
     */
74
    private $database = '';
75
76
    /**
77
     * @var RequestFactoryInterface
78
     */
79
    private $requestFactory;
80
81
    /**
82
     * @var ResponseFactoryInterface
83
     */
84
    private $responseFactory;
85
86
    /**
87
     * @var StreamFactoryInterface
88
     */
89
    private $streamFactory;
90
91
    /**
92
     * @param array|ClientOptions $options
93
     * @param RequestFactoryInterface $requestFactory
94
     * @param ResponseFactoryInterface $responseFactory
95
     * @param StreamFactoryInterface $streamFactory
96
     */
97 44
    public function __construct(
98
        $options,
99
        RequestFactoryInterface $requestFactory,
100
        ResponseFactoryInterface $responseFactory,
101
        StreamFactoryInterface $streamFactory
102
    ) {
103 44
        $this->options = $options instanceof ClientOptions ? $options : new ClientOptions($options);
104 44
        $this->useKeepAlive = ($this->options[ClientOptions::OPTION_CONNECTION] === 'Keep-Alive');
105 44
        $this->requestFactory = $requestFactory;
106 44
        $this->responseFactory = $responseFactory;
107 44
        $this->streamFactory = $streamFactory;
108 44
        $this->updateCommonHttpHeaders();
109 44
    }
110
111
    /**
112
     * Delegate / shorthand method
113
     *
114
     * @param RequestInterface $request
115
     * @return ResponseInterface
116
     * @throws \Psr\Http\Client\ClientExceptionInterface
117
     */
118
    public function __invoke(RequestInterface $request): ResponseInterface
119
    {
120
        return $this->sendRequest($request);
121
    }
122
123
    public function sendType(Type $type): ResponseInterface
124
    {
125
        $guard = null;
126
127
        if ($type instanceof GuardSupport) {
128
            $guard = $type->guard();
129
        }
130
131
        $response = $this->sendRequest($type->toRequest($this->requestFactory, $this->streamFactory));
132
133
        if ($guard !== null) {
134
            $guard($response);
135
        }
136
137
        return $response;
138
    }
139
140 42
    public function sendRequest(RequestInterface $request): ResponseInterface
141
    {
142
        try {
143 42
            $stream = $request->getBody();
144 42
            $body = $stream->getContents();
145 42
            $method = $request->getMethod();
146
147 42
            $customHeaders = $request->getHeaders();
148 42
            unset($customHeaders['Connection'], $customHeaders['Content-Length']);
149
150 42
            if (! isset($customHeaders['Content-Type'])) {
151 7
                $customHeaders['Content-Type'] = ['application/json'];
152
            }
153
154 42
            $customHeader = '';
155 42
            foreach ($customHeaders as $headerKey => $headerValues) {
156 42
                foreach ($headerValues as $headerValue) {
157 42
                    $customHeader .= $headerKey . ': ' . $headerValue . self::EOL;
158
                }
159
            }
160
        } catch (\Throwable $e) {
161
            throw RequestFailedException::ofRequest($request, $e);
162
        }
163
164 42
        $customHeader .= 'Content-Length: ' . $stream->getSize() . self::EOL;
165
166 42
        $url = $this->baseUrl . $request->getUri();
167
168
        try {
169 42
            $this->open($request);
170
171 42
            $result = $this->transmit(
172 42
                $method . ' ' . $url . ' HTTP/1.1' .
173 42
                $this->headerLines .
174 42
                $customHeader . self::EOL .
175 42
                $body,
176 42
                $method
177
            );
178
            // TODO https://docs.arangodb.com/3.4/Manual/Architecture/DeploymentModes/ActiveFailover/Architecture.html
179 42
            $status = stream_get_meta_data($this->handle);
180
181 42
            if (true === $status['timed_out']) {
182
                throw TimeoutException::ofRequest($request);
183
            }
184 42
            if (! $this->useKeepAlive) {
185
                $this->close();
186
            }
187
188 42
            [$httpCode, $headers, $body] = HttpHelper::parseMessage($result);
189
        } catch (\Throwable $e) {
190
            throw NetworkException::with($request, $e);
191
        }
192 42
        $response = $this->responseFactory->createResponse($httpCode);
193
194 42
        foreach ($headers as $headerName => $header) {
195 42
            $response = $response->withAddedHeader($headerName, $header);
196
        }
197
198 42
        return $response->withBody($this->streamFactory->createStream($body));
199
    }
200
201
    /**
202
     * Sends request to server and reads response.
203
     *
204
     * @param string $request
205
     * @param string $method
206
     * @return string
207
     */
208 42
    private function transmit(string $request, string $method): string
209
    {
210 42
        fwrite($this->handle, $request);
211 42
        fflush($this->handle);
212
213 42
        $contentLength = 0;
214 42
        $bodyLength = 0;
215 42
        $readTotal = 0;
216 42
        $matches = [];
217 42
        $message = '';
218
219
        do {
220 42
            $read = fread($this->handle, self::CHUNK_SIZE);
221 42
            if (false === $read || $read === '') {
222
                break;
223
            }
224 42
            $readLength = strlen($read);
225 42
            $readTotal += $readLength;
226 42
            $message .= $read;
227
228 42
            if ($contentLength === 0
229 42
                && $method !== 'HEAD'
230 42
                && 1 === preg_match('/content-length: (\d+)/i', $message, $matches)
231
            ) {
232 42
                $contentLength = (int)$matches[1];
233
            }
234
235 42
            if ($bodyLength === 0) {
236 42
                $bodyStart = strpos($message, "\r\n\r\n");
237
238 42
                if (false !== $bodyStart) {
239 42
                    $bodyLength = $bodyStart + $contentLength + 4;
240
                }
241
            }
242 42
        } while ($readTotal < $bodyLength && ! feof($this->handle));
243
244 42
        return $message;
245
    }
246
247
    /**
248
     * Update common HTTP headers for all HTTP requests
249
     */
250 44
    private function updateCommonHttpHeaders(): void
251
    {
252 44
        $this->headerLines = self::EOL;
253
254 44
        $endpoint = $this->options[ClientOptions::OPTION_ENDPOINT];
255 44
        if (1 !== preg_match('/^unix:\/\/.+/', $endpoint)) {
256 44
            $this->headerLines .= 'Host: '
257 44
                . preg_replace('/^(tcp|ssl):\/\/(.+?):(\d+)\/?$/', '\\2', $endpoint)
258 44
                . self::EOL;
259
        }
260
        // add basic auth header
261
        if (isset(
262 44
            $this->options[ClientOptions::OPTION_AUTH_TYPE],
263 44
            $this->options[ClientOptions::OPTION_AUTH_USER]
264
        )) {
265
            $this->headerLines .= sprintf(
266
                'Authorization: %s %s%s',
267
                $this->options[ClientOptions::OPTION_AUTH_TYPE],
268
                base64_encode(
269
                    $this->options[ClientOptions::OPTION_AUTH_USER] . ':' .
270
                    $this->options[ClientOptions::OPTION_AUTH_PASSWD]
271
                ),
272
                self::EOL
273
            );
274
        }
275
276 44
        if (isset($this->options[ClientOptions::OPTION_CONNECTION])) {
277 44
            $this->headerLines .= 'Connection: ' . $this->options[ClientOptions::OPTION_CONNECTION] . self::EOL;
278
        }
279
280 44
        $this->database = $this->options[ClientOptions::OPTION_DATABASE];
281 44
        $this->baseUrl = '/_db/' . urlencode($this->database);
282 44
    }
283
284
    /**
285
     * Opens connection depending on options.
286
     *
287
     * @param RequestInterface $request
288
     */
289 42
    private function open(RequestInterface $request): void
290
    {
291 42
        if ($this->useKeepAlive && $this->handle !== null && is_resource($this->handle)) {
292 18
            if (! feof($this->handle)) {
293 18
                return;
294
            }
295
296
            $this->close();
297
298
            if (false === $this->options[ClientOptions::OPTION_RECONNECT]) {
299
                throw ConnectionException::forRequest(
300
                    $request,
301
                    'Server has closed the connection already.',
302
                    StatusCodeInterface::STATUS_REQUEST_TIMEOUT
303
                );
304
            }
305
        }
306
307 42
        $endpoint = $this->options[ClientOptions::OPTION_ENDPOINT];
308 42
        $context = stream_context_create();
309
310 42
        if (1 === preg_match('/^ssl:\/\/.+/', $endpoint)) {
311
            stream_context_set_option(
312
                $context,
313
                [
314
                    'ssl' => [
315
                        'verify_peer' => $this->options[ClientOptions::OPTION_VERIFY_CERT],
316
                        'verify_peer_name' => $this->options[ClientOptions::OPTION_VERIFY_CERT_NAME],
317
                        'allow_self_signed' => $this->options[ClientOptions::OPTION_ALLOW_SELF_SIGNED],
318
                        'ciphers' => $this->options[ClientOptions::OPTION_CIPHERS],
319
                    ],
320
                ]
321
            );
322
        }
323
324 42
        $handle = stream_socket_client(
325 42
            $endpoint,
326 42
            $errNo,
327 42
            $message,
328 42
            $this->options[ClientOptions::OPTION_TIMEOUT],
329 42
            STREAM_CLIENT_CONNECT,
330 42
            $context
331
        );
332
333 42
        if (false === $handle) {
334
            throw ConnectionException::forRequest(
335
                $request,
336
                sprintf('Cannot connect to endpoint "%s". Message: %s', $endpoint, $message),
337
                $errNo
338
            );
339
        }
340 42
        $this->handle = $handle;
341 42
        stream_set_timeout($this->handle, $this->options[ClientOptions::OPTION_TIMEOUT]);
342 42
    }
343
344
    /**
345
     * Closes connection
346
     */
347
    private function close(): void
348
    {
349
        fclose($this->handle);
350
        unset($this->handle);
351
    }
352
}
353