Completed
Pull Request — master (#7)
by Sandro
02:21
created

Statement::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 15
ccs 7
cts 8
cp 0.875
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 4
crap 2.0078
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
namespace ArangoDb;
11
12
use ArangoDb\Exception\ServerException;
13
use ArangoDb\Http\JsonStream;
14
use ArangoDb\Util\Json;
15
use Countable;
16
use Fig\Http\Message\RequestMethodInterface;
17
use Fig\Http\Message\StatusCodeInterface;
18
use Iterator;
19
use Psr\Http\Client\ClientExceptionInterface;
20
use Psr\Http\Client\ClientInterface;
21
use Psr\Http\Message\RequestFactoryInterface;
22
use Psr\Http\Message\RequestInterface;
23
24
final class Statement implements Iterator, Countable
25
{
26
    /**
27
     * "objectType" option entry.
28
     */
29
    public const ENTRY_TYPE = 'objectType';
30
31
    public const ENTRY_TYPE_JSON = 'json';
32
    public const ENTRY_TYPE_ARRAY = 'array';
33
    public const ENTRY_TYPE_OBJECT = 'object';
34
35
    /**
36
     * Entry id for cursor id
37
     */
38
    private const ENTRY_ID = 'id';
39
40
    /**
41
     * Whether or not to get more documents
42
     */
43
    private const ENTRY_HAS_MORE = 'hasMore';
44
45
    /**
46
     * Result documents
47
     */
48
    private const ENTRY_RESULT = 'result';
49
50
    /**
51
     * Extra data
52
     */
53
    private const ENTRY_EXTRA = 'extra';
54
55
    /**
56
     * Stats
57
     */
58
    private const ENTRY_STATS = 'stats';
59
60
    /**
61
     * Full count (ignoring the outermost LIMIT)
62
     */
63
    private const FULL_COUNT = 'fullCount';
64
65
    /**
66
     * Whether or not the result was served from the AQL query cache
67
     */
68
    private const ENTRY_CACHED = 'cached';
69
70
    /**
71
     * @var ClientInterface
72
     */
73
    private $client;
74
75
    /**
76
     * Cursor options
77
     *
78
     * @var array
79
     */
80
    private $options;
81
82
    /**
83
     * @var RequestFactoryInterface
84
     */
85
    private $requestFactory;
86
87
    /**
88
     * @var mixed
89
     */
90
    private $data;
91
92
    /**
93
     * @var bool
94
     */
95
    private $hasMore = true;
96
97
    /**
98
     * cursor id
99
     *
100
     * @var string
101
     */
102
    private $id;
103
104
    /**
105
     * Current position in result set iteration (zero-based)
106
     *
107
     * @var int
108
     */
109
    private $position;
110
111
    /**
112
     * Total length of result set (in number of documents)
113
     *
114
     * @var int
115
     */
116
    private $length;
117
118
    /**
119
     * Full count of the result set (ignoring the outermost LIMIT)
120
     *
121
     * @var int|null
122
     */
123
    private $fullCount;
124
125
    /**
126
     * Extra data (statistics) returned from the statement
127
     *
128
     * @var array
129
     */
130
    private $extra = [];
131
132
    /**
133
     * Number of HTTP calls that were made to build the cursor result
134
     *
135
     * @var int
136
     */
137
    private $fetches = 0;
138
139
    /**
140
     * Whether or not the query result was served from the AQL query result cache
141
     *
142
     * @var bool
143
     */
144
    private $cached = false;
145
146
    /**
147
     * @var RequestInterface
148
     */
149
    private $request;
150
151
    /**
152
     * @var bool
153
     */
154
    private $executed = false;
155
156
    /**
157
     * Query is executed on first access
158
     *
159
     * @param ClientInterface $client - connection to be used
160
     * @param RequestInterface $request Cursor request
161
     * @param RequestFactoryInterface $requestFactory
162
     * @param array $options
163
     */
164 7
    public function __construct(
165
        ClientInterface $client,
166
        RequestInterface $request,
167
        RequestFactoryInterface $requestFactory,
168
        array $options = []
169
    ) {
170 7
        if (! isset($options[self::ENTRY_TYPE])) {
171
            $options[self::ENTRY_TYPE] = self::ENTRY_TYPE_JSON;
172
        }
173
174 7
        $this->client = $client;
175 7
        $this->options = $options;
176 7
        $this->requestFactory = $requestFactory;
177 7
        $this->request = $request;
178 7
        $this->data = [];
179 7
    }
180
181
    /**
182
     * Fetch outstanding results from the server
183
     *
184
     * @return void
185
     * @throws ClientExceptionInterface
186
     */
187 7
    private function fetchOutstanding(): void
188
    {
189 7
        $request = $this->fetches === 0
190 7
            ? $this->request
191 7
            : $this->requestFactory->createRequest(RequestMethodInterface::METHOD_PUT, Url::CURSOR . '/' . $this->id);
192
193 7
        $response = $this->client->sendRequest($request);
194
195 7
        $httpStatusCode = $response->getStatusCode();
196
197 7
        if ($httpStatusCode < StatusCodeInterface::STATUS_OK
198 7
            || $httpStatusCode > StatusCodeInterface::STATUS_MULTIPLE_CHOICES
199
        ) {
200
            throw ServerException::with($request, $response);
201
        }
202
203 7
        $this->fetches++;
204
205 7
        $data = $response->getBody();
206 7
        $tmp = $data->getContents();
207 7
        $data = $data instanceof JsonStream ? $data->toArray() : Json::decode($tmp);
208
209 7
        if (isset($data[self::ENTRY_ID])) {
210 3
            $this->id = $data[self::ENTRY_ID];
211
        }
212
213 7
        if (isset($data[self::ENTRY_EXTRA])) {
214 7
            $this->extra = $data[self::ENTRY_EXTRA];
215
216 7
            if (isset($this->extra[self::ENTRY_STATS][self::FULL_COUNT])) {
217
                $this->fullCount = $this->extra[self::ENTRY_STATS][self::FULL_COUNT];
218
            }
219
        }
220
221 7
        if (isset($data[self::ENTRY_CACHED])) {
222 7
            $this->cached = $data[self::ENTRY_CACHED];
223
        }
224 7
        $this->hasMore = $data[self::ENTRY_HAS_MORE] ?? false;
225
226 7
        $this->length += count($data[self::ENTRY_RESULT]);
227
228 7
        $this->data = array_merge($this->data, $data[self::ENTRY_RESULT]);
229
230 7
        if (false === $this->hasMore) {
231 7
            unset($this->id);
232
        }
233 7
    }
234
235
    /**
236
     * Return the current result row depending on entry type
237
     *
238
     * This might issue additional HTTP requests to fetch any outstanding results from the server
239
     *
240
     * @return string|array|object Data
241
     * @throws ClientExceptionInterface
242
     */
243 5
    public function fetchAll()
244
    {
245 5
        while ($this->hasMore) {
246 5
            $this->fetchOutstanding();
247
        }
248
249 5
        switch ($this->options[self::ENTRY_TYPE]) {
250 5
            case self::ENTRY_TYPE_OBJECT:
251
                return (object)$this->data;
252 5
            case self::ENTRY_TYPE_ARRAY:
253 3
                return $this->data;
254 2
            case self::ENTRY_TYPE_JSON:
255
            default:
256 2
                return Json::encode($this->data);
257
        }
258
    }
259
260
    /**
261
     * Get the total number of results in the cursor.
262
     *
263
     * This might issue additional HTTP requests to fetch any outstanding results from the server.
264
     *
265
     * @return int Total number of results
266
     * @throws ClientExceptionInterface
267
     */
268
    public function count()
269
    {
270
        while ($this->hasMore) {
271
            $this->fetchOutstanding();
272
        }
273
274
        return $this->length;
275
    }
276
277
    /**
278
     * Rewind the cursor, loads first batch, can be repeated (new cursor will be created)
279
     *
280
     * @return void
281
     * @throws ClientExceptionInterface
282
     */
283 3
    public function rewind()
284
    {
285 3
        $this->length = 0;
286 3
        $this->fetches = 0;
287 3
        $this->position = 0;
288 3
        $this->executed = false;
289 3
        $this->hasMore = true;
290
291 3
        $this->data = [];
292 3
        $this->fetchOutstanding();
293 3
    }
294
295
    /**
296
     * Return the current result row depending on entry type
297
     *
298
     * @return string|array|object Data
299
     */
300 3
    public function current()
301
    {
302 3
        switch ($this->options[self::ENTRY_TYPE]) {
303 3
            case self::ENTRY_TYPE_OBJECT:
304
                return (object)$this->data[$this->position];
305 3
            case self::ENTRY_TYPE_ARRAY:
306 3
                return $this->data[$this->position];
307
            case self::ENTRY_TYPE_JSON:
308
            default:
309
                return Json::encode($this->data[$this->position]);
310
        }
311
    }
312
313 3
    public function key(): int
314
    {
315 3
        return $this->position;
316
    }
317
318 3
    public function next(): void
319
    {
320 3
        $this->position++;
321 3
    }
322
323
    /**
324
     * @return bool
325
     * @throws ClientExceptionInterface
326
     */
327 3
    public function valid(): bool
328
    {
329 3
        if ($this->position <= $this->length - 1) {
330
            // we have more results than the current position is
331 3
            return true;
332
        }
333
334 3
        if (! $this->hasMore || $this->id === null) {
335 3
            return false;
336
        }
337
338
        // need to fetch additional results from the server
339 2
        $this->fetchOutstanding();
340
341 2
        return ($this->position <= $this->length - 1);
342
    }
343
344
    /**
345
     * Returns the extra data of the query (statistics etc.). Contents of the result array depend on the type of query
346
     * executed
347
     *
348
     * @return array
349
     */
350
    public function extra(): array
351
    {
352
        return $this->extra ?? [];
353
    }
354
355
    /**
356
     * Returns the warnings issued during query execution
357
     *
358
     * @return array
359
     */
360
    public function warnings(): array
361
    {
362
        return $this->extra['warnings'] ?? [];
363
    }
364
365
    /**
366
     * Returns the number of writes executed by the query
367
     *
368
     * @return int
369
     */
370
    public function writesExecuted(): int
371
    {
372
        return $this->getStatValue('writesExecuted');
373
    }
374
375
    /**
376
     * Returns the number of ignored write operations from the query
377
     *
378
     * @return int
379
     */
380
    public function writesIgnored(): int
381
    {
382
        return $this->getStatValue('writesIgnored');
383
    }
384
385
    /**
386
     * Returns the number of documents iterated over in full scans
387
     *
388
     * @return int
389
     */
390
    public function scannedFull(): int
391
    {
392
        return $this->getStatValue('scannedFull');
393
    }
394
395
    /**
396
     * Returns the number of documents iterated over in index scans
397
     *
398
     * @return int
399
     */
400
    public function scannedIndex(): int
401
    {
402
        return $this->getStatValue('scannedIndex');
403
    }
404
405
    /**
406
     * Returns the number of documents filtered by the query
407
     *
408
     * @return int
409
     */
410
    public function filtered(): int
411
    {
412
        return $this->getStatValue('filtered');
413
    }
414
415
    /**
416
     * Returns the number of HTTP calls that were made to build the cursor result
417
     *
418
     * @return int
419
     */
420 7
    public function fetches(): int
421
    {
422 7
        return $this->fetches;
423
    }
424
425
    /**
426
     * Returns cursor id only after first rewind / fetch
427
     *
428
     * @return string
429
     */
430
    public function getId(): ?string
431
    {
432
        return $this->id;
433
    }
434
435
    /**
436
     * Get the full count of the cursor if available. Does not load all data.
437
     *
438
     * @return int Total number of results
439
     */
440
    public function fullCount(): ?int
441
    {
442
        return $this->fullCount;
443
    }
444
445
    /**
446
     * Get the cached attribute for the result set
447
     *
448
     * @return bool Whether or not the query result was served from the AQL query cache
449
     */
450
    public function isCached(): bool
451
    {
452
        return $this->cached;
453
    }
454
455
    /**
456
     * Returns statistical figure value from the query result, default is 0
457
     *
458
     * @param string $name Name of figure
459
     *
460
     * @return int
461
     */
462
    private function getStatValue(string $name): int
463
    {
464
        return $this->extra[self::ENTRY_STATS][$name] ?? 0;
465
    }
466
}
467