1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace mindplay\sql\framework; |
4
|
|
|
|
5
|
|
|
use Iterator; |
6
|
|
|
use IteratorAggregate; |
7
|
|
|
use RuntimeException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* This class represents the result of fetching a `PreparedStatement`, e.g. the results of |
11
|
|
|
* a `SELECT` SQL query, and with Mappers being applied on-the-fly, in batches. |
12
|
|
|
* |
13
|
|
|
* It implements `IteratorAggregate`, allowing you to execute the query and iterate |
14
|
|
|
* over the result set with a `foreach` statement. |
15
|
|
|
*/ |
16
|
|
|
class Result implements IteratorAggregate |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* @var PreparedStatement |
20
|
|
|
*/ |
21
|
|
|
private $statement; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var int |
25
|
|
|
*/ |
26
|
|
|
private $batch_size; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var Mapper[] list of Mappers to apply when fetching results |
30
|
|
|
*/ |
31
|
|
|
private $mappers; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var Indexer|null |
35
|
|
|
*/ |
36
|
|
|
private $indexer = null; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @param PreparedStatement $statement prepared statement |
40
|
|
|
* @param int $batch_size batch-size (when fetching large result sets) |
41
|
|
|
* @param Mapper[] $mappers list of Mappers to apply while fetching results |
42
|
|
|
* @param Indexer|null $indexer optional Indexer (used to customize Generator keys) |
43
|
|
|
*/ |
44
|
1 |
|
public function __construct(PreparedStatement $statement, $batch_size, array $mappers, Indexer $indexer = null) |
45
|
|
|
{ |
46
|
1 |
|
$this->statement = $statement; |
47
|
1 |
|
$this->batch_size = $batch_size; |
48
|
1 |
|
$this->mappers = $mappers; |
49
|
1 |
|
$this->indexer = $indexer; |
50
|
1 |
|
} |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @return mixed|null first record of the record-set (or NULL, if the record-set is empty) |
54
|
|
|
*/ |
55
|
1 |
|
public function firstRow() |
56
|
|
|
{ |
57
|
1 |
|
foreach ($this->createIterator(1) as $record) { |
58
|
1 |
|
return $record; // break from loop immediately after fetching the first record |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
return null; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @return mixed|null first column value of the first record of the record-set (or NULL, if the record-set is empty) |
66
|
|
|
*/ |
67
|
1 |
|
public function firstCol() |
68
|
|
|
{ |
69
|
1 |
|
foreach ($this->createIterator(1) as $record) { |
70
|
1 |
|
$keys = array_keys($record); |
71
|
|
|
|
72
|
1 |
|
return $record[$keys[0]]; // break from loop immediately after fetching the first record |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
return null; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @return array all the records of the record-set |
80
|
|
|
* |
81
|
|
|
* @throws RuntimeException if the result-set contains overlapping keys (generated by an Indexer) |
82
|
|
|
*/ |
83
|
1 |
|
public function all() |
84
|
|
|
{ |
85
|
1 |
|
if ($this->indexer === null) { |
86
|
1 |
|
return iterator_to_array($this->getIterator()); // performance optimization: no index-check required |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
// NOTE: an Indexer can generate duplicate keys, for example if a foreign key |
90
|
|
|
// is used in a query that returns multiple records with the same key. |
91
|
|
|
// |
92
|
|
|
// validate the result and throw an exception if we find a duplicate: |
93
|
|
|
|
94
|
1 |
|
$result = []; |
95
|
|
|
|
96
|
1 |
|
foreach ($this->getIterator() as $key => $record) { |
97
|
1 |
|
if (isset($result[$key])) { |
98
|
|
|
throw new RuntimeException("duplicate key: {$key}"); |
99
|
|
|
} |
100
|
|
|
|
101
|
1 |
|
$result[$key] = $record; |
102
|
|
|
} |
103
|
|
|
|
104
|
1 |
|
return $result; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Execute this Statement and return a Generator, so you can iterate over the results. |
109
|
|
|
* |
110
|
|
|
* This method implements `IteratorAggregate`, permitting you to iterate directly over |
111
|
|
|
* the resulting records (or objects) without explicitly having to call this method. |
112
|
|
|
* |
113
|
|
|
* @return Iterator |
114
|
|
|
*/ |
115
|
1 |
|
public function getIterator() |
116
|
|
|
{ |
117
|
1 |
|
return $this->createIterator($this->batch_size); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Create an Iterator with a given batch-size. |
122
|
|
|
* |
123
|
|
|
* @param int $batch_size batch-size when processing the result set |
124
|
|
|
* |
125
|
|
|
* @return Iterator |
126
|
|
|
*/ |
127
|
1 |
|
private function createIterator($batch_size) |
128
|
|
|
{ |
129
|
1 |
|
$fetching = true; |
130
|
|
|
|
131
|
|
|
do { |
132
|
|
|
// fetch a batch of records: |
133
|
|
|
|
134
|
1 |
|
$batch = []; |
135
|
|
|
|
136
|
|
|
do { |
137
|
1 |
|
$record = $this->statement->fetch(); |
138
|
|
|
|
139
|
1 |
|
if ($record) { |
|
|
|
|
140
|
1 |
|
$batch[] = $record; |
141
|
|
|
} else { |
142
|
1 |
|
if (count($batch) === 0) { |
143
|
1 |
|
return; // last batch of records fetched |
144
|
|
|
} |
145
|
|
|
|
146
|
1 |
|
$fetching = false; // last record of batch fetched |
147
|
|
|
} |
148
|
1 |
|
} while ($fetching && (count($batch) < $batch_size)); |
149
|
|
|
|
150
|
|
|
// apply Mappers to current batch of records: |
151
|
|
|
|
152
|
1 |
|
$num_records = count($batch); |
153
|
|
|
|
154
|
1 |
|
foreach ($this->mappers as $index => $mapper) { |
155
|
1 |
|
$batch = $mapper->map($batch); |
156
|
|
|
|
157
|
1 |
|
if (count($batch) !== $num_records) { |
158
|
|
|
$count = count($batch); |
159
|
|
|
|
160
|
1 |
|
throw new RuntimeException("Mapper #{$index} returned {$count} records, expected: {$num_records}"); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
// return each record from the current batch: |
165
|
|
|
|
166
|
1 |
|
if ($this->indexer) { |
167
|
1 |
|
foreach ($batch as $record) { |
168
|
1 |
|
$index = $this->indexer->index($record); |
169
|
|
|
|
170
|
1 |
|
yield $index => $record; |
171
|
|
|
} |
172
|
|
|
} else { |
173
|
1 |
|
foreach ($batch as $record) { |
174
|
1 |
|
yield $record; |
175
|
|
|
} |
176
|
|
|
} |
177
|
1 |
|
} while ($fetching); |
178
|
1 |
|
} |
179
|
|
|
} |
180
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.