Passed
Pull Request — master (#7)
by Sandro
04:09
created

Client::open()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 16.8542

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 53
ccs 14
cts 29
cp 0.4828
rs 8.1475
c 0
b 0
f 0
cc 8
nc 10
nop 1
crap 16.8542

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