Result::firstRow()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 10
1
<?php
2
3
namespace mindplay\sql\framework;
4
5
use Iterator;
6
use IteratorAggregate;
7
use RuntimeException;
8
use Traversable;
9
10
/**
11
 * This class represents the result of fetching a `PreparedStatement`, e.g. the results of
12
 * a `SELECT` SQL query, and with Mappers being applied on-the-fly, in batches.
13
 *
14
 * It implements `IteratorAggregate`, allowing you to execute the query and iterate
15
 * over the result set with a `foreach` statement.
16
 * 
17
 * @implements IteratorAggregate<array<string,mixed>>
18
 */
19
class Result implements IteratorAggregate
20
{
21
    private PreparedStatement $statement;
22
    private int $batch_size;
23
24
    /**
25
     * @var Mapper[] list of Mappers to apply when fetching results
26
     */
27
    private array $mappers;
28
29
    /**
30
     * @param PreparedStatement $statement  prepared statement
31
     * @param int               $batch_size batch-size (when fetching large result sets)
32
     * @param Mapper[]          $mappers    list of Mappers to apply while fetching results
33
     */
34 1
    public function __construct(PreparedStatement $statement, int $batch_size, array $mappers)
35
    {
36 1
        $this->statement = $statement;
37 1
        $this->batch_size = $batch_size;
38 1
        $this->mappers = $mappers;
39
    }
40
41
    /**
42
     * @return array<string,mixed>|null first record of the record-set (or NULL, if the record-set is empty)
43
     */
44 1
    public function firstRow(): array|null
45
    {
46 1
        foreach ($this->createIterator(1) as $record) {
47 1
            return $record; // break from loop immediately after fetching the first record
48
        }
49
50
        return null;
51
    }
52
53
    /**
54
     * @return mixed|null first column value of the first record of the record-set (or NULL, if the record-set is empty)
55
     */
56 1
    public function firstCol(): mixed
57
    {
58 1
        foreach ($this->createIterator(1) as $record) {
59 1
            $keys = array_keys($record);
60
61 1
            return $record[$keys[0]]; // break from loop immediately after fetching the first record
62
        }
63
64
        return null;
65
    }
66
67
    /**
68
     * @return array<array<string,mixed>> all the records of the record-set
69
     */
70 1
    public function all(): array
71
    {
72 1
        return iterator_to_array($this->getIterator());
73
    }
74
    
75
    /**
76
     * Execute this Statement and return a Generator, so you can iterate over the results.
77
     *
78
     * This method implements `IteratorAggregate`, permitting you to iterate directly over
79
     * the resulting records (or objects) without explicitly having to call this method.
80
     */
81 1
    public function getIterator(): Traversable
82
    {
83 1
        return $this->createIterator($this->batch_size);
84
    }
85
86
    /**
87
     * Create an Iterator with a given batch-size.
88
     *
89
     * @param int $batch_size batch-size when processing the result set
90
     *
91
     * @return Iterator
92
     */
93 1
    private function createIterator($batch_size): Iterator
94
    {
95 1
        $fetching = true;
96
97 1
        $record_index = 0;
98
99
        do {
100
            // fetch a batch of records:
101
102 1
            $batch = [];
103
104
            do {
105 1
                $record = $this->statement->fetch();
106
107 1
                if ($record) {
108 1
                    $batch[$record_index++] = $record;
109
                } else {
110 1
                    if (count($batch) === 0) {
111 1
                        return; // last batch of records fetched
112
                    }
113
114 1
                    $fetching = false; // last record of batch fetched
115
                }
116 1
            } while ($fetching && (count($batch) < $batch_size));
117
118
            // apply Mappers to current batch of records:
119
120 1
            $num_records = count($batch);
121
122 1
            foreach ($this->mappers as $mapper_index => $mapper) {
123 1
                $batch = $mapper->map($batch);
124
125 1
                if ($batch instanceof Traversable) {
126 1
                    $batch = iterator_to_array($batch);
127
                }
128
129 1
                if (count($batch) !== $num_records) {
130
                    $count = count($batch);
131
132
                    throw new RuntimeException("Mapper #{$mapper_index} returned {$count} records, expected: {$num_records}");
133
                }
134
            }
135
136
            // return each record from the current batch:
137
138 1
            foreach ($batch as $index => $record) {
139 1
                yield $index => $record;
140
            }
141 1
        } while ($fetching);
142
    }
143
}
144