Recordset::initialize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 10
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EngineWorks\DBAL;
6
7
use Countable;
8
use EngineWorks\DBAL\Internal\ConvertObjectToStringMethod;
9
use InvalidArgumentException;
10
use IteratorAggregate;
11
use LogicException;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use RuntimeException;
15
16
/**
17
 * Recordset class
18
 * Hint: Use DBAL::queryRecordset() instead of using this class directly
19
 * @implements IteratorAggregate<int|string, array<string, mixed>>
20
 */
21
class Recordset implements LoggerAwareInterface, IteratorAggregate, Countable
22
{
23
    use ConvertObjectToStringMethod;
1 ignored issue
show
introduced by
The trait EngineWorks\DBAL\Interna...ertObjectToStringMethod requires some properties which are not provided by EngineWorks\DBAL\Recordset: $value, $name
Loading history...
24
25
    public const RSMODE_NOTCONNECTED = 0;
26
27
    public const RSMODE_CONNECTED_EDIT = 1;
28
29
    public const RSMODE_CONNECTED_ADDNEW = 2;
30
31
    /**
32
     * Associative array of the current record
33
     * @var array<string, scalar|null>
34
     */
35
    public $values;
36
37
    /** @var DBAL */
38
    private $dbal;
39
40
    /**
41
     * Never use this property, use self::getLogger() instead because
42
     * when Logger is NULL it uses the DBAL::getLogger().
43
     *
44
     * @var LoggerInterface|null
45
     */
46
    private $logger;
47
48
    /**
49
     * @var Result|null
50
     */
51
    private $result;
52
53
    /**
54
     * Array of original values
55
     * @var array<string, scalar|null>|null
56
     */
57
    private $originalValues;
58
59
    /**
60
     * source sql query
61
     * @var string
62
     */
63
    private $source;
64
65
    /** @var int */
66
    private $mode;
67
68
    /**
69
     * Has the name of the current entity
70
     * @var string
71
     */
72
    private $entity;
73
74
    /**
75
     * This array is a local copy of $this->result->getFields()
76
     * @var array<string, array{name: string, table: string, commontype: string}>
77
     */
78
    private $datafields;
79
80
    /**
81
     * Storage of idFields, set after call query method
82
     * @var string[]
83
     */
84
    private $idFields;
85
86
    /**
87
     * Recordset constructor.
88
     *
89
     * @param DBAL $dbal
90
     * @param LoggerInterface|null $logger If not provided it uses the DBAL Logger
91
     */
92 45
    public function __construct(DBAL $dbal, LoggerInterface $logger = null)
93
    {
94 45
        $this->dbal = $dbal;
95 45
        $this->logger = $logger;
96 45
        $this->initialize();
97
    }
98
99
    /**
100
     * Return the current logger, uses the DBAL logger when local logger is not set
101
     *
102
     * @return LoggerInterface
103
     */
104 3
    public function getLogger(): LoggerInterface
105
    {
106 3
        return $this->logger ?? $this->dbal->getLogger();
107
    }
108
109
    /**
110
     * Define the current logger, if NULL uses the DBAL logger
111
     *
112
     * @param LoggerInterface|null $logger
113
     */
114 1
    public function setLogger(?LoggerInterface $logger): void
115
    {
116 1
        $this->logger = $logger;
117
    }
118
119
    /**
120
     * Executes a SQL Query and connect the object with that query in order to operate the recordset
121
     * @param string $sql
122
     * @param string $overrideEntity
123
     * @param string[] $overrideKeys
124
     * @param string[] $overrideTypes
125
     *
126
     * @return true
127
     */
128 43
    final public function query(
129
        string $sql,
130
        string $overrideEntity = '',
131
        array $overrideKeys = [],
132
        array $overrideTypes = []
133
    ): bool {
134 43
        $this->initialize();
135 43
        if (! $this->hasDBAL()) {
136 1
            throw new LogicException('Recordset: object does not have a connected DBAL');
137
        }
138 42
        $result = $this->dbal->queryResult($sql, $overrideTypes);
139 42
        if (! $result instanceof Result) {
140 12
            throw new LogicException("Recordset: Unable to perform query $sql");
141
        }
142 30
        $this->mode = self::RSMODE_CONNECTED_EDIT;
143 30
        $this->result = $result;
144 30
        $this->source = $sql;
145 30
        $this->datafields = [];
146
        // get fields into a temporary array
147
        /** @var array<array{name: string, table: string, commontype: string}> $tmpfields */
148 30
        $tmpfields = $this->result()->getFields();
149 30
        foreach ($tmpfields as $tmpfield) {
150 30
            $this->datafields[$tmpfield['name']] = $tmpfield;
151
        }
152
        // set the entity name, remove if more than one table exists
153 30
        if (count(array_unique(array_column($tmpfields, 'table'))) > 1) {
154
            $this->entity = '';
155
        } else {
156 30
            $this->entity = (string) $tmpfields[0]['table'];
157
        }
158 30
        if ('' !== $overrideEntity) {
159 8
            $this->entity = $overrideEntity;
160
        }
161
        // set the id fields if did not override
162 30
        if ([] === $overrideKeys) {
163 26
            $this->idFields = $this->result()->getIdFields() ?: [];
164
        } else {
165
            // validate overrideKeys
166 8
            if ($overrideKeys !== array_unique($overrideKeys)) {
167
                throw new InvalidArgumentException('Keys contains repeated values');
168
            }
169 8
            foreach ($overrideKeys as $fieldName) {
170 8
                if (! is_string($fieldName)) {
171
                    throw new InvalidArgumentException('Keys were set but at least one is not a string');
172
                }
173 8
                if (! array_key_exists($fieldName, $this->datafields)) {
174
                    throw new InvalidArgumentException(
175
                        "The field name $fieldName does not exists in the set of fields"
176
                    );
177
                }
178
            }
179 8
            $this->idFields = $overrideKeys;
180
        }
181
        // if it has records then load first
182 30
        if ($this->getRecordCount() > 0) {
183 18
            $this->moveNext();
184
        }
185 30
        return true;
186
    }
187
188
    /**
189
     * Internal procedure to initiate all the variables
190
     */
191 45
    private function initialize(): void
192
    {
193 45
        $this->entity = '';
194 45
        $this->source = '';
195 45
        $this->mode = self::RSMODE_NOTCONNECTED;
196 45
        $this->result = null;
197 45
        $this->originalValues = null;
198 45
        $this->datafields = [];
199 45
        $this->values = [];
200 45
        $this->idFields = [];
201
    }
202
203
    /**
204
     * Return if the current DBAL and Result exists and are connected
205
     * @return bool
206
     */
207 18
    final public function isOpen(): bool
208
    {
209 18
        return ($this->hasDBAL() && null !== $this->result);
210
    }
211
212
    /**
213
     * Check if the DBAL instance is connected (if not try to connect again)
214
     * @return bool
215
     */
216 43
    final public function hasDBAL(): bool
217
    {
218 43
        return ($this->dbal->isConnected() || $this->dbal->connect());
219
    }
220
221
    /**
222
     * Return the source query
223
     * @return string
224
     */
225 4
    final public function getSource(): string
226
    {
227 4
        return $this->source;
228
    }
229
230
    /**
231
     * Return the recordset mode
232
     * @return int
233
     */
234 4
    final public function getMode(): int
235
    {
236 4
        return $this->mode;
237
    }
238
239
    /**
240
     * Return if the current recordset can be edited
241
     * @return bool
242
     */
243 4
    public function canModify(): bool
244
    {
245 4
        return (self::RSMODE_NOTCONNECTED !== $this->mode && '' !== $this->entity);
246
    }
247
248
    /**
249
     * Return if the recordset is placed in a valid record
250
     * @phpstan-impure
251
     * @return bool
252
     */
253 20
    final public function eof(): bool
254
    {
255 20
        return (! is_array($this->originalValues));
256
    }
257
258
    /**
259
     * Return the original value of a field
260
     * @param scalar|null $defaultValue
261
     * @return scalar|null
262
     */
263 5
    final public function getOriginalValue(string $fieldName, $defaultValue = '')
264
    {
265 5
        if (! is_array($this->originalValues) || ! array_key_exists($fieldName, $this->originalValues)) {
266 4
            return $defaultValue;
267
        }
268 5
        return $this->originalValues[$fieldName];
269
    }
270
271
    /**
272
     * Return an array with the original values.
273
     *
274
     * @return array<string, scalar|null>
275
     * @throws RuntimeException There are no original values
276
     */
277 4
    final public function getOriginalValues(): array
278
    {
279 4
        if (! is_array($this->originalValues)) {
280 4
            throw new RuntimeException('There are no original values');
281
        }
282 4
        return $this->originalValues;
283
    }
284
285
    /**
286
     * Prepares the recordset to make an insertion
287
     * All the values are set to null
288
     */
289 8
    final public function addNew(): void
290
    {
291 8
        $this->originalValues = null;
292 8
        $this->values = $this->emptyValuesFromDataFields();
293 8
        $this->mode = self::RSMODE_CONNECTED_ADDNEW;
294
    }
295
296
    /**
297
     * Get the last inserted id by asking the DBAL object.
298
     * This means that if an insertion happends between Update and LastInsertedID then the result
299
     * will not be related to the Update
300
     * @return int
301
     */
302 1
    final public function lastInsertedID(): int
303
    {
304 1
        return $this->dbal->lastInsertedID();
305
    }
306
307
    /**
308
     * Check whether the current values are different from the original ones
309
     * The base are the original values
310
     * @return bool
311
     * @throws RuntimeException if no original values exists
312
     */
313 2
    final public function valuesHadChanged(): bool
314
    {
315 2
        if (! is_array($this->originalValues)) {
316
            throw new RuntimeException('The recordset does not contain any original values');
317
        }
318 2
        foreach ($this->originalValues as $field => $value) {
319 2
            $current = array_key_exists($field, $this->values) ? $this->values[$field] : null;
320 2
            if (static::valueIsDifferent($value, $current)) {
321 2
                return true;
322
            }
323
        }
324 1
        return false;
325
    }
326
327
    /**
328
     * Compare values in order to see if they need to be updated
329
     * @param object|scalar|null $original
330
     * @param object|scalar|null $current
331
     * @return bool
332
     */
333 6
    final protected static function valueIsDifferent($original, $current): bool
334
    {
335
        // check if some value is null
336 6
        $originalIsNull = (null === $original);
337 6
        $currentIsNull = (null === $current);
338
        // both are null, there are no difference
339 6
        if ($originalIsNull && $currentIsNull) {
340
            return false;
341
        }
342
        // one is null, the other isn't, there is a difference
343 6
        if ($originalIsNull || $currentIsNull) {
344 4
            return true;
345
        }
346
        // do not continue using the object, convert to string
347 6
        if (is_object($current)) {
348
            $current = self::convertObjectToString($current);
349
        }
350
        // strict comparison if types are strings
351 6
        if (is_string($original) && is_string($current)) {
352 6
            return ($original !== $current);
353
        }
354
        // simple comparison
355 6
        return ($original != $current);
356
    }
357
358
    /**
359
     * @return string[]
360
     */
361 5
    public function getIdFields(): array
362
    {
363 5
        return $this->idFields;
364
    }
365
366
    /**
367
     * Create an array of conditions based on the current values and ids
368
     * This function is used on Update and on Delete
369
     * @param string $extraWhereClause
370
     * @return string[]
371
     */
372 5
    protected function sqlWhereConditions(string $extraWhereClause): array
373
    {
374
        // get the conditions
375 5
        $conditions = [];
376 5
        if ($extraWhereClause) {
377
            $conditions[] = "($extraWhereClause)";
378
        }
379 5
        $ids = $this->getIdFields();
380 5
        if ([] === $ids) {
381
            $this->getLogger()->warning(sprintf(
382
                'Recordset: the where clause will be based on all fields because cannot locate ids.%s',
383
                "\n" . print_r(['entity' => $this->entity, 'values' => $this->values], true)
0 ignored issues
show
Bug introduced by
Are you sure print_r(array('entity' =...> $this->values), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

383
                "\n" . /** @scrutinizer ignore-type */ print_r(['entity' => $this->entity, 'values' => $this->values], true)
Loading history...
384
            ));
385
            $ids = array_keys($this->datafields);
386
        }
387 5
        foreach ($this->datafields as $fieldname => $field) {
388 5
            if (! array_key_exists($fieldname, $this->values)) {
389
                continue;
390
            }
391 5
            if (! in_array($fieldname, $ids)) {
392 5
                continue;
393
            }
394 5
            $originalValue = $this->getOriginalValue($fieldname, null);
395 5
            if (null === $originalValue) {
396
                $conditions[] = '(' . $this->dbal->sqlIsNull($this->dbal->sqlFieldEscape($fieldname)) . ')';
397
            } else {
398 5
                $conditions[] = '(' . $this->dbal->sqlFieldEscape($fieldname) . ' = '
399 5
                    . $this->dbal->sqlQuote($originalValue, $field['commontype'], false) . ')';
400
            }
401
        }
402 5
        return $conditions;
403
    }
404
405
    /**
406
     * Create the sql statement for INSERT INTO
407
     * @return string
408
     */
409 8
    protected function sqlInsert(): string
410
    {
411 8
        $inserts = [];
412 8
        foreach ($this->datafields as $fieldname => $field) {
413 8
            $value = (array_key_exists($fieldname, $this->values)) ? $this->values[$fieldname] : null;
414 8
            $escapedFieldName = $this->dbal->sqlFieldEscape($field['name']);
415 8
            $inserts[$escapedFieldName] = $this->dbal->sqlQuote($value, $field['commontype'], true);
416
        }
417 8
        if ([] === $inserts) {
418
            throw new LogicException('Recordset: Insert does not have any fields to insert');
419
        }
420 8
        return 'INSERT INTO ' . $this->dbal->sqlTableEscape($this->entity)
421 8
        . ' (' . implode(', ', array_keys($inserts)) . ')'
422 8
        . ' VALUES (' . implode(', ', $inserts) . ')'
423 8
        . ';';
424
    }
425
426
    /**
427
     * Create the sql statement for UPDATE
428
     * If nothing to update then will return an empty string
429
     * @param string $extraWhereClause
430
     * @return string
431
     */
432 5
    protected function sqlUpdate(string $extraWhereClause): string
433
    {
434
        // get the conditions to alter the current record
435 5
        $conditions = $this->sqlWhereConditions($extraWhereClause);
436
        // if no conditions then log error and return false
437 5
        if ([] === $conditions) {
438
            throw new LogicException('Recordset: The current record does not have any conditions to update');
439
        }
440
        // get the fields that have changed compared to originalValues
441 5
        $updates = [];
442 5
        foreach ($this->datafields as $fieldname => $field) {
443 5
            if (! array_key_exists($fieldname, $this->values)) {
444
                $this->values[$fieldname] = null;
445
            }
446 5
            if (static::valueIsDifferent($this->getOriginalValue($fieldname, null), $this->values[$fieldname])) {
447 5
                $updates[] = $this->dbal->sqlFieldEscape($fieldname) . ' = '
448 5
                    . $this->dbal->sqlQuote($this->values[$fieldname], $field['commontype'], true);
449
            }
450
        }
451
        // if nothing to update, log error and return empty string
452 5
        if ([] === $updates) {
453
            return '';
454
        }
455
        // return the update statement
456 5
        return 'UPDATE'
457 5
            . ' ' . $this->dbal->sqlTableEscape($this->entity)
458 5
            . ' SET ' . implode(', ', $updates)
459 5
            . ' WHERE ' . implode(' AND ', $conditions)
460 5
            . ';';
461
    }
462
463
    /**
464
     * Create the sql statement for DELETE
465
     * @param string $extraWhereClause
466
     * @return string
467
     */
468 4
    protected function sqlDelete(string $extraWhereClause): string
469
    {
470
        // get the conditions to alter the current record
471 4
        $conditions = $this->sqlWhereConditions($extraWhereClause);
472
        // if no conditions then log error and return false
473 4
        if ([] === $conditions) {
474
            throw new LogicException('Recordset: The current record does not have any conditions to delete');
475
        }
476 4
        return 'DELETE'
477 4
            . ' FROM ' . $this->dbal->sqlTableEscape($this->entity)
478 4
            . ' WHERE ' . implode(' AND ', $conditions)
479 4
            . ';';
480
    }
481
482
    /**
483
     * Build and execute an SQL UPDATE or INSERT sentence
484
     * Return how many rows where altered, if an update does not change any value then it returns zero
485
     * Return false in case of error execution
486
     *
487
     * @param string $extraWhereClause where clause to be appended into sql on UPDATE (not insert)
488
     * @return int|false
489
     */
490 9
    final public function update(string $extraWhereClause = '')
491
    {
492
        // check the current mode is on ADDNEW or EDIT
493 9
        if (self::RSMODE_CONNECTED_ADDNEW !== $this->mode && self::RSMODE_CONNECTED_EDIT !== $this->mode) {
494
            throw new LogicException(
495
                "Recordset: The recordset is not on edit or addnew mode [current: {$this->mode}]"
496
            );
497
        }
498
        // check the entity is not empty
499 9
        if ('' === $this->entity) {
500
            throw new LogicException('Recordset: The recordset does not have a valid unique entity');
501
        }
502 9
        $sql = '';
503 9
        if (self::RSMODE_CONNECTED_ADDNEW == $this->mode) {
504 8
            $sql = $this->sqlInsert();
505
        }
506 9
        if (self::RSMODE_CONNECTED_EDIT == $this->mode) {
507 5
            if ('' === $sql = $this->sqlUpdate($extraWhereClause)) {
508
                return 0;
509
            }
510
        }
511 9
        $altered = $this->dbal->execute($sql);
512 9
        if (0 === $altered) {
513 1
            $diffs = [];
514 1
            if (is_array($this->originalValues)) {
515 1
                foreach ($this->originalValues as $name => $value) {
516 1
                    if (! static::valueIsDifferent($value, $this->values[$name])) {
517 1
                        continue;
518
                    }
519 1
                    $diffs[] = $name;
520
                }
521
            }
522 1
            $this->getLogger()->warning(print_r([
0 ignored issues
show
Bug introduced by
It seems like print_r(array('message' ...iffs' => $diffs), true) can also be of type true; however, parameter $message of Psr\Log\LoggerInterface::warning() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

522
            $this->getLogger()->warning(/** @scrutinizer ignore-type */ print_r([
Loading history...
523 1
                'message' => "Recordset: The statement $sql return zero affected rows but the values are different",
524 1
                'entity' => $this->entity,
525 1
                'extraWhereClause' => $extraWhereClause,
526 1
                'original' => $this->originalValues,
527 1
                'current' => $this->values,
528 1
                'diffs' => $diffs,
529 1
            ], true));
530
        }
531 9
        return $altered;
532
    }
533
534
    /**
535
     * Build and execute the SQL DELETE sentence
536
     * Return how many rows where altered
537
     *
538
     * @return int|false
539
     */
540 4
    final public function delete(string $extraWhereClause = '')
541
    {
542 4
        if (self::RSMODE_CONNECTED_EDIT !== $this->mode) {
543
            throw new LogicException('Recordset: The recordset is not on edit mode [current: ' . $this->mode . ']');
544
        }
545
        // check the entity is not empty
546 4
        if ('' === $this->entity) {
547
            throw new LogicException('Recordset: The recordset does not have a valid unique entity');
548
        }
549 4
        $sql = $this->sqlDelete($extraWhereClause);
550 4
        $altered = $this->dbal->execute($sql);
551 4
        if (0 === $altered) {
552
            $this->getLogger()->warning(print_r([
0 ignored issues
show
Bug introduced by
It seems like print_r(array('message' ...>originalValues), true) can also be of type true; however, parameter $message of Psr\Log\LoggerInterface::warning() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

552
            $this->getLogger()->warning(/** @scrutinizer ignore-type */ print_r([
Loading history...
553
                'message' => "Recordset: The statement '$sql' return zero affected rows"
554
                    . ' but it should delete at least one record',
555
                'entity' => $this->entity,
556
                'extraWhereClause' => $extraWhereClause,
557
                'original' => $this->originalValues,
558
            ], true));
559
        }
560 4
        return $altered;
561
    }
562
563
    /**
564
     * Move to the next row and read the values
565
     * @return bool
566
     */
567 18
    final public function moveNext(): bool
568
    {
569 18
        return ($this->isOpen() && $this->fetchLoadValues());
570
    }
571
572
    /**
573
     * Move to the first row and read the values
574
     * @return bool
575
     */
576 4
    final public function moveFirst(): bool
577
    {
578 4
        return ($this->isOpen() && $this->result()->moveFirst() && $this->fetchLoadValues());
579
    }
580
581
    /**
582
     * Internal function that returns an array with the content from fields and row
583
     * @return array<string, null>
584
     */
585 8
    private function emptyValuesFromDataFields(): array
586
    {
587 8
        return array_fill_keys(array_keys($this->datafields), null);
588
    }
589
590
    /**
591
     * Internal function that returns an array with the content of all datafields
592
     * filled with the values cast
593
     *
594
     * @param array<string, scalar|null> $source
595
     * @return array<string, scalar|null>
596
     */
597 18
    private function setValuesFromDatafields(array $source): array
598
    {
599 18
        $values = [];
600 18
        foreach ($this->datafields as $fieldname => $field) {
601 18
            $values[$fieldname] = $this->castValueWithCommonType(
602 18
                array_key_exists($fieldname, $source) ? $source[$fieldname] : null,
603 18
                $field['commontype']
604 18
            );
605
        }
606 18
        return $values;
607
    }
608
609
    /**
610
     * Cast a generic value from the source to a typed value, if null return null
611
     *
612
     * @param scalar|null $value
613
     * @param string $commonType
614
     * @return scalar|null
615
     */
616 18
    protected function castValueWithCommonType($value, string $commonType)
617
    {
618
        // these are sorted by the most common data types to avoid extra comparisons
619 18
        if (null === $value) {
620 4
            return null;
621
        }
622 18
        if (CommonTypes::TTEXT === $commonType) {
623 17
            return strval($value);
624
        }
625 18
        if (CommonTypes::TINT === $commonType) {
626 18
            return intval($value);
627
        }
628 17
        if (CommonTypes::TNUMBER === $commonType) {
629 17
            return floatval($value);
630
        }
631 13
        if (CommonTypes::TBOOL === $commonType) {
632 13
            return boolval($value);
633
        }
634 13
        if (in_array($commonType, [CommonTypes::TDATE, CommonTypes::TTIME, CommonTypes::TDATETIME], true)) {
635 13
            return strtotime((string) $value);
636
        }
637
        return strval($value);
638
    }
639
640
    /**
641
     * Try to load values fetching a new row
642
     * Return true if success
643
     * Return false if no row was fetched, also put values to an empty array
644
     * @return bool
645
     */
646 18
    private function fetchLoadValues(): bool
647
    {
648 18
        $row = $this->result()->fetchRow();
649 18
        if (false === $row) {
650 8
            $this->originalValues = null;
651 8
            $this->values = [];
652 8
            return false;
653
        }
654 18
        $this->originalValues = $this->setValuesFromDatafields($row);
655 18
        $this->values = $this->originalValues;
656 18
        return true;
657
    }
658
659
    /**
660
     * Return the number of records in the query
661
     * @return int
662
     */
663 30
    final public function getRecordCount(): int
664
    {
665 30
        return $this->result()->resultCount();
666
    }
667
668
    /**
669
     * Return an associative array of fields, the key is the field name
670
     * and the content is an array containing name, common type and table
671
     * @return array<string, array{name: string, table: string, commontype: string}>
672
     */
673
    final public function getFields(): array
674
    {
675
        return $this->datafields;
676
    }
677
678
    /**
679
     * Return the entity name of the query
680
     * @return string
681
     */
682 4
    final public function getEntityName(): string
683
    {
684 4
        return $this->entity;
685
    }
686
687 4
    final public function count(): int
688
    {
689 4
        return $this->getRecordCount();
690
    }
691
692 4
    final public function getIterator(): Iterators\RecordsetIterator
693
    {
694 4
        return new Iterators\RecordsetIterator($this);
695
    }
696
697
    /**
698
     * @return Result
699
     */
700 30
    private function result(): Result
701
    {
702 30
        if (null === $this->result) {
703
            throw new RuntimeException('The current state of the result is NULL');
704
        }
705 30
        return $this->result;
706
    }
707
}
708