Failed Conditions
Push — 42-add-disconnect ( 4b8118...3c6c37 )
by Bas
02:18
created

ArangoClient::request()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
nc 8
nop 4
dl 0
loc 20
rs 9.9
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArangoClient;
6
7
use ArangoClient\Exceptions\ArangoException;
8
use ArangoClient\Http\HttpClientConfig;
9
use ArangoClient\Http\HttpRequestOptions;
10
use ArangoClient\Statement\Statement;
11
use ArangoClient\Transactions\SupportsTransactions;
12
use GuzzleHttp\Client as GuzzleClient;
13
use GuzzleHttp\Exception\GuzzleException;
14
use GuzzleHttp\Exception\RequestException;
15
use Psr\Http\Message\ResponseInterface;
16
use Spatie\DataTransferObject\Exceptions\UnknownProperties;
17
use stdClass;
18
use Throwable;
19
use Traversable;
20
21
/**
22
 * The arangoClient handles connections to ArangoDB's HTTP REST API.
23
 *
24
 * @see https://www.arangodb.com/docs/stable/http/
25
 */
26
class ArangoClient
27
{
28
    use HandlesResponses;
29
    use HandlesJson;
30
    use HasManagers;
31
    use SupportsTransactions;
32
33
    protected GuzzleClient $httpClient;
34
35
    protected HttpClientConfig $config;
36
37
    /**
38
     * ArangoClient constructor.
39
     *
40
     * @param  array<string|numeric|null>  $config
41
     * @param  GuzzleClient|null  $httpClient
42
     *
43
     * @throws UnknownProperties
44
     */
45
    public function __construct(array $config = [], ?GuzzleClient $httpClient = null)
46
    {
47
        $config['endpoint'] = $this->generateEndpoint($config);
48
        $this->config = new HttpClientConfig($config);
49
50
        $this->httpClient = $httpClient ?? new GuzzleClient($this->config->mapGuzzleHttpClientConfig());
51
    }
52
53
    public function __destruct()
54
    {
55
        $this->disconnect();
56
    }
57
58
    public function disconnect(): bool
59
    {
60
        $config = $this->getConfig();
61
62
        if ($config['connection'] !== 'Keep-Alive') {
63
            return true;
64
        }
65
66
        $response = $this->rawRequest(
67
            'HEAD',
68
            '/_api/version',
69
            [
70
                'headers' => [
71
                    'Connection' => 'close',
72
                ],
73
            ],
74
        );
75
76
        if ($response === null) {
77
            return false;
78
        }
79
80
        $connection = $response->getHeader('Connection');
81
        if (reset($connection) !== 'Close') {
82
            return false;
83
        }
84
85
        return true;
86
    }
87
88
89
    /**
90
     * @param  array<mixed>  $config
91
     */
92
    public function generateEndpoint(array $config): string
93
    {
94
        if (isset($config['endpoint'])) {
95
            return (string) $config['endpoint'];
96
        }
97
98
        $endpoint = 'http://localhost:8529';
99
        if (isset($config['host'])) {
100
            $endpoint = (string) $config['host'];
101
        }
102
103
        if (isset($config['port'])) {
104
            $endpoint .= ':' . (string) $config['port'];
105
        }
106
107
        return $endpoint;
108
    }
109
110
    /**
111
     * @param  array<mixed>|HttpRequestOptions  $options
112
     *
113
     * @throws ArangoException
114
     */
115
    public function request(string $method, string $uri, array|HttpRequestOptions $options = [], ?string $database = null): stdClass
116
    {
117
        $uri = $this->prependDatabaseToUri($uri, $database);
118
119
        if (is_array($options)) {
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
120
            $options = $this->prepareRequestOptions($options);
121
        }
122
123
        $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
124
        try {
125
            $response = $this->httpClient->request($method, $uri, $options->all());
126
        } catch (Throwable $e) {
127
            $this->handleGuzzleException($e);
128
        }
129
130
        if ($response !== null) {
131
            return $this->cleanupResponse($response);
132
        }
133
134
        return new stdClass();
135
    }
136
137
    /**
138
     * @param  array<mixed>|HttpRequestOptions  $options
139
     *
140
     * @throws ArangoException
141
     */
142
    public function rawRequest(string $method, string $uri, array|HttpRequestOptions $options = []): ResponseInterface|null
143
    {
144
        if (is_array($options)) {
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
145
            $options = $this->prepareRequestOptions($options);
146
        }
147
148
        $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
149
        try {
150
            $response = $this->httpClient->request($method, $uri, $options->all());
151
        } catch (Throwable $e) {
152
            $this->handleGuzzleException($e);
153
        }
154
155
        return $response;
156
    }
157
158
159
    /**
160
     * @param  array<mixed>  $options
161
     *
162
     * @throws ArangoException
163
     */
164
    protected function prepareRequestOptions(array $options): HttpRequestOptions
165
    {
166
        if (isset($options['body'])) {
167
            $options['body'] = $this->jsonEncode($options['body']);
168
        }
169
170
        return new HttpRequestOptions($options);
171
    }
172
173
    /**
174
     * Return the response with debug information (for internal testing purposes).
175
     *
176
     * @param  array<mixed>  $options
177
     *
178
     * @throws GuzzleException
179
     */
180
    public function debugRequest(
181
        string $method,
182
        string $uri,
183
        array $options = [],
184
        ?string $database = null,
185
    ): ResponseInterface {
186
        $uri = $this->prependDatabaseToUri($uri, $database);
187
        $options['debug'] = true;
188
189
        return $this->httpClient->request($method, $uri, $options);
190
    }
191
192
    protected function prependDatabaseToUri(string $uri, ?string $database = null): string
193
    {
194
        if (!isset($database)) {
195
            $database = $this->config->database;
196
        }
197
198
        return '/_db/' . urlencode($database) . $uri;
199
    }
200
201
    /**
202
     * @throws ArangoException
203
     */
204
    protected function handleGuzzleException(Throwable $e): void
205
    {
206
        $message = $e->getMessage();
207
        $code = $e->getCode();
208
209
        if ($e instanceof RequestException && $e->hasResponse()) {
210
            $response = $e->getResponse();
211
            if ($response !== null) {
212
                $decodedResponse = $this->decodeResponse($response);
213
            }
214
            if (isset($decodedResponse->errorMessage)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $decodedResponse does not seem to be defined for all execution paths leading up to this point.
Loading history...
215
                $message = (string) $decodedResponse->errorMessage;
216
            }
217
218
            if (isset($decodedResponse->code)) {
219
                $code = (int) $decodedResponse->code;
220
            }
221
        }
222
223
        throw(
224
            new ArangoException(
225
                $code . ' - ' . $message,
226
                $code,
227
            )
228
        );
229
    }
230
231
    /**
232
     * @SuppressWarnings(PHPMD.StaticAccess)
233
     */
234
    protected function cleanupResponse(ResponseInterface $response): stdClass
235
    {
236
        $response = $this->decodeResponse($response);
237
        unset($response->error);
238
        unset($response->code);
239
240
        return $response;
241
    }
242
243
    /**
244
     * @param  array<scalar>  $bindVars
245
     * @param  array<mixed>  $options
246
     * @return Statement
247
     */
248
    public function prepare(
249
        string $query,
250
        array $bindVars = [],
251
        array $options = [],
252
    ): Traversable {
253
        return new Statement($this, $query, $bindVars, $options);
254
    }
255
256
    /**
257
     * @return mixed
258
     */
259
    public function getConfig(?string $value = null): mixed
260
    {
261
        if ($value) {
262
            return $this->config->$value;
263
        }
264
        return $this->config->toArray();
265
    }
266
267
    public function setDatabase(string $name): void
268
    {
269
        $this->config->database = $name;
270
    }
271
272
    public function getDatabase(): string
273
    {
274
        return $this->config->database;
275
    }
276
277
    public function setHttpClient(GuzzleClient $httpClient): void
278
    {
279
        $this->httpClient = $httpClient;
280
    }
281
282
    public function getHttpClient(): GuzzleClient
283
    {
284
        return $this->httpClient;
285
    }
286
287
    public function getUser(): string
288
    {
289
        return (string) $this->config->username;
290
    }
291
}
292