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

Client::open()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 53
ccs 17
cts 34
cp 0.5
rs 8.1475
c 0
b 0
f 0
cc 8
nc 10
nop 1
crap 16

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