Result::createIterator()   B
last analyzed

Complexity

Conditions 10
Paths 17

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 10.0751

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 24
c 1
b 0
f 0
nc 17
nop 1
dl 0
loc 49
ccs 20
cts 22
cp 0.9091
crap 10.0751
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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