Completed
Pull Request — master (#34)
by Rasmus
05:13
created

Result::all()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 8
cts 9
cp 0.8889
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 9
nc 4
nop 0
crap 4.0218
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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