Failed Conditions
Push — next ( 580d6f...1a59b4 )
by Bas
02:36
created

ArangoClient::setHttpClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArangoClient;
6
7
use ArangoClient\Admin\AdminManager;
8
use ArangoClient\Exceptions\ArangoException;
9
use ArangoClient\Schema\SchemaManager;
10
use ArangoClient\Statement\Statement;
11
use ArangoClient\Transactions\SupportsTransactions;
12
use GuzzleHttp\Client;
13
use GuzzleHttp\Exception\RequestException;
14
use GuzzleHttp\Psr7\StreamWrapper;
15
use JsonMachine\JsonMachine;
16
use Psr\Http\Message\ResponseInterface;
17
use Throwable;
18
use Traversable;
19
20
/**
21
 * The arangoClient handles connections to ArangoDB's HTTP REST API.
22
 *
23
 * @see https://www.arangodb.com/docs/stable/http/
24
 */
25
class ArangoClient
26
{
27
    use SupportsTransactions;
28
29
    protected Client $httpClient;
30
31
    /**
32
     * @var string
33
     */
34
    protected string $endpoint;
35
36
    /**
37
     * @var array<mixed>|false
38
     */
39
    protected $allowRedirects;
40
41
    /**
42
     * @var float
43
     */
44
    protected float $connectTimeout;
45
46
    /**
47
     * @var string
48
     */
49
    protected string $connection;
50
51
    /**
52
     * @var string|null
53
     */
54
    protected ?string $username = null;
55
56
    /**
57
     * @var string|null
58
     */
59
    protected ?string $password = null;
60
61
    /**
62
     * @var string
63
     */
64
    protected string $database;
65
66
    /**
67
     * @var AdminManager|null
68
     */
69
    protected ?AdminManager $adminManager = null;
70
71
    /**
72
     * @var SchemaManager|null
73
     */
74
    protected ?SchemaManager $schemaManager = null;
75
76
    /**
77
     * ArangoClient constructor.
78
     *
79
     * @param  array<string|numeric|null>  $config
80
     * @param  Client|null  $httpClient
81
     */
82 76
    public function __construct(array $config = [], Client $httpClient = null)
83
    {
84 76
        $this->endpoint = $this->generateEndpoint($config);
85 76
        $this->username = (isset($config['username'])) ? (string) $config['username'] : null;
86 76
        $this->password = (isset($config['password'])) ? (string) $config['password'] : null;
87 76
        $this->database = (isset($config['database'])) ? (string) $config['database'] : '_system';
88 76
        $this->connection = (isset($config['connection'])) ? (string) $config['connection'] : 'Keep-Alive';
89 76
        $this->allowRedirects = (isset($config['allow_redirects'])) ? (array) $config['allow_redirects'] : false;
90 76
        $this->connectTimeout = (isset($config['connect_timeout'])) ? (float) $config['connect_timeout'] : 0;
91
92 76
        $this->httpClient = isset($httpClient) ? $httpClient : new Client($this->mapHttpClientConfig());
93 76
    }
94
95
    /**
96
     * @param  array<mixed>  $config
97
     * @return string
98
     */
99 76
    public function generateEndpoint(array $config): string
100
    {
101 76
        if (isset($config['endpoint'])) {
102
            return (string) $config['endpoint'];
103
        }
104
105 76
        $endpoint = (isset($config['host'])) ? (string) $config['host'] : 'http://localhost';
106 76
        $endpoint .= (isset($config['port'])) ? ':' . (string) $config['port'] : ':8529';
107
108 76
        return $endpoint;
109
    }
110
111
    /**
112
     * @return array<array<mixed>|string|numeric|bool|null>
113
     */
114 76
    protected function mapHttpClientConfig(): array
115
    {
116 76
        $config = [];
117 76
        $config['base_uri'] = $this->endpoint;
118 76
        $config['allow_redirects'] = $this->allowRedirects;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->allowRedirects can also be of type boolean. However, the property $allowRedirects is declared as type array<mixed,mixed>|false. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
119 76
        $config['connect_timeout'] = $this->connectTimeout;
120 76
        $config['auth'] = [
121 76
            $this->username,
122 76
            $this->password,
123
        ];
124 76
        $config['header'] = [
125 76
            'Connection' => $this->connection
126
        ];
127 76
        return $config;
128
    }
129
130
    /**
131
     * @psalm-suppress MixedReturnStatement
132
     *
133
     * @param  string  $method
134
     * @param  string  $uri
135
     * @param  array<mixed>  $options
136
     * @param  string|null  $database
137
     * @return array<mixed>
138
     * @throws ArangoException
139
     */
140 76
    public function request(string $method, string $uri, array $options = [], ?string $database = null): array
141
    {
142 76
        $uri = $this->prependDatabaseToUri($uri, $database);
143
144 76
        $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
145
        try {
146 76
            $response = $this->httpClient->request($method, $uri, $options);
147 1
        } catch (\Throwable $e) {
148 1
            $this->handleGuzzleException($e);
149
        }
150
151 76
        return $this->decodeResponse($response);
152
    }
153
154
    /**
155
     * @return array<array<mixed>|string|numeric|bool|null>
156
     */
157 1
    public function getConfig(): array
158
    {
159 1
        $config = [];
160 1
        $config['endpoint'] = $this->endpoint;
161 1
        $config['username'] = $this->username;
162 1
        $config['password'] = $this->password;
163 1
        $config['database'] = $this->database;
164 1
        $config['connection'] = $this->connection;
165 1
        $config['allow_redirects'] = $this->allowRedirects;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->allowRedirects can also be of type boolean. However, the property $allowRedirects is declared as type array<mixed,mixed>|false. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
166 1
        $config['connect_timeout'] = $this->connectTimeout;
167
168 1
        return $config;
169
    }
170
171
    /**
172
     * @psalm-suppress MixedAssignment, MixedArrayOffset
173
     * @SuppressWarnings(PHPMD.StaticAccess)
174
     *
175
     * @param  ResponseInterface|null  $response
176
     * @return array<mixed>
177
     */
178 76
    protected function decodeResponse(?ResponseInterface $response): array
179
    {
180 76
        if (! isset($response)) {
181
            return [];
182
        }
183
184 76
        $decodedResponse = [];
185
186 76
        $phpStream = StreamWrapper::getResource($response->getBody());
187 76
        $decodedStream = JsonMachine::fromStream($phpStream);
188
189 76
        foreach ($decodedStream as $key => $value) {
190 76
            $decodedResponse[$key] = $value;
191
        }
192
193 76
        return $decodedResponse;
194
    }
195
196
    /**
197
     * @param  array<mixed>  $data
198
     * @return string
199
     * @throws ArangoException
200
     */
201 21
    public function jsonEncode(array $data): string
202
    {
203 21
        $response = '';
204
205 21
        if (! empty($data)) {
206 21
            $response = json_encode($data);
207
        }
208 21
        if (empty($data)) {
209
            $response = json_encode($data, JSON_FORCE_OBJECT);
210
        }
211
212 21
        if ($response === false) {
0 ignored issues
show
introduced by
The condition $response === false is always false.
Loading history...
213
            throw new ArangoException('JSON encoding failed with error: ' . json_last_error_msg(), json_last_error());
214
        }
215 21
        return $response;
216
    }
217
218
    /**
219
     * @return string
220
     */
221 76
    public function getUser(): string
222
    {
223 76
        return (string) $this->username;
224
    }
225
226
    /**
227
     * @param string $name
228
     * @return void
229
     */
230 76
    public function setDatabase(string $name): void
231
    {
232 76
        $this->database = $name;
233 76
    }
234
235
    /**
236
     * @return string
237
     */
238 1
    public function getDatabase(): string
239
    {
240 1
        return $this->database;
241
    }
242
243
    /**
244
     * @param  string  $query
245
     * @param  array<scalar>  $bindVars
246
     * @param  array<mixed>  $options
247
     * @return Traversable<mixed>
248
     */
249 14
    public function prepare(
250
        string $query,
251
        array $bindVars = [],
252
        array $options = []
253
    ): Traversable {
254 14
        return new Statement($this, $query, $bindVars, $options);
255
    }
256
257
    /**
258
     * @return AdminManager
259
     */
260 8
    public function admin(): AdminManager
261
    {
262 8
        if (! isset($this->adminManager)) {
263 8
            $this->adminManager = new AdminManager($this);
264
        }
265 8
        return $this->adminManager;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->adminManager could return the type null which is incompatible with the type-hinted return ArangoClient\Admin\AdminManager. Consider adding an additional type-check to rule them out.
Loading history...
266
    }
267
268 76
    public function schema(): SchemaManager
269
    {
270 76
        if (! isset($this->schemaManager)) {
271 76
            $this->schemaManager = new SchemaManager($this);
272
        }
273 76
        return $this->schemaManager;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schemaManager could return the type null which is incompatible with the type-hinted return ArangoClient\Schema\SchemaManager. Consider adding an additional type-check to rule them out.
Loading history...
274
    }
275
276
    /**
277
     * @param  Throwable  $e
278
     * @throws ArangoException
279
     */
280 1
    protected function handleGuzzleException(Throwable $e): void
281
    {
282 1
        $message = $e->getMessage();
283 1
        $code = $e->getCode();
284
285 1
        if ($e instanceof RequestException && $e->hasResponse()) {
286 1
            $decodedResponse = $this->decodeResponse($e->getResponse());
287 1
            $message = (string) $decodedResponse['errorMessage'];
288 1
            $code = (int) $decodedResponse['code'];
289
        }
290
291
        throw(
292 1
            new ArangoException(
293 1
                $message,
294 1
                (int) $code
295
            )
296
        );
297
    }
298
299 76
    protected function prependDatabaseToUri(string $uri, ?string $database = null): string
300
    {
301 76
        if (! isset($database)) {
302 76
            $database = $this->database;
303
        }
304 76
        return '/_db/' . urlencode($database) . $uri;
305
    }
306
307 1
    public function setHttpClient(Client $httpClient): void
308
    {
309 1
        $this->httpClient = $httpClient;
310 1
    }
311
312 1
    public function getHttpClient(): Client
313
    {
314 1
        return $this->httpClient;
315
    }
316
}
317