PDOConnection   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 87.34%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 75
c 5
b 0
f 0
dl 0
loc 228
ccs 69
cts 79
cp 0.8734
rs 9.92
wmc 31

11 Methods

Rating   Name   Duplication   Size   Complexity  
A count() 0 3 1
A logQuery() 0 4 2
A addLogger() 0 3 1
A lastInsertId() 0 13 3
B transact() 0 49 10
A fetch() 0 10 2
A __construct() 0 4 1
A expandPlaceholders() 0 19 5
A getPDO() 0 3 1
A prepare() 0 25 4
A execute() 0 7 1
1
<?php
2
3
namespace mindplay\sql\framework\pdo;
4
5
use mindplay\sql\framework\PreparedStatement;
6
use Throwable;
7
use Exception;
8
use mindplay\sql\exceptions\TransactionAbortedException;
9
use mindplay\sql\framework\Connection;
10
use mindplay\sql\framework\Countable;
11
use mindplay\sql\framework\Logger;
12
use mindplay\sql\framework\MapperProvider;
13
use mindplay\sql\framework\Result;
14
use mindplay\sql\framework\Statement;
15
use mindplay\sql\model\TypeProvider;
16
use PDO;
17
use UnexpectedValueException;
18
19
/**
20
 * This class implements a Connection adapter for a PDO connection.
21
 */
22
abstract class PDOConnection implements Connection, PDOExceptionMapper, Logger
23
{
24
    private PDO $pdo;
25
26
    private TypeProvider $types;
27
    
28
    /**
29
     * @var int number of nested calls to transact()
30
     *
31
     * @see transact()
32
     */
33
    private int $transaction_level = 0;
34
35
    /**
36
     * @var bool net result of nested calls to transact()
37
     *
38
     * @see transact()
39
     */
40
    private bool $transaction_result = true;
41
42
    /**
43
     * @var Logger[]
44
     */
45
    private array $loggers = [];
46
47
    /**
48
     * To avoid duplicating dependencies, you should use DatabaseContainer::createPDOConnection()
49
     * rather than calling this constructor directly.
50
     *
51
     * @param PDO          $pdo
52
     * @param TypeProvider $types
53
     */
54 1
    public function __construct(PDO $pdo, TypeProvider $types)
55
    {
56 1
        $this->pdo = $pdo;
57 1
        $this->types = $types;
58
    }
59
60
    /**
61
     * @return PDO the internal PDO connection object
62
     */
63
    public function getPDO(): PDO
64
    {
65
        return $this->pdo;
66
    }
67
68
    /**
69
     * @inheritdoc
70
     */
71 1
    public function prepare(Statement $statement): PreparedStatement
72
    {
73 1
        $params = $statement->getParams();
74
75 1
        $sql = $this->expandPlaceholders($statement->getSQL(), $params);
76
77 1
        $prepared_statement = new PreparedPDOStatement($this->pdo->prepare($sql), $this, $this->types, $this);
78
        
79 1
        foreach ($params as $name => $value) {
80 1
            if (is_array($value)) {
81 1
                $index = 1; // use a base-1 offset consistent with expandPlaceholders()
82
83 1
                foreach ($value as $item) {
84
                    // NOTE: we deliberately ignore the array indices here, as using them could result in broken SQL!
85
86 1
                    $prepared_statement->bind("{$name}_{$index}", $item);
87
88 1
                    $index += 1;
89
                }
90
            } else {
91 1
                $prepared_statement->bind($name, $value);
92
            }
93
        }
94
95 1
        return $prepared_statement;
96
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101 1
    public function fetch(Statement $statement, int $batch_size = 1000): Result
102
    {
103 1
        $mappers = $statement instanceof MapperProvider
104 1
            ? $statement->getMappers()
105
            : [];
106
        
107 1
        return new Result(
108 1
            $this->prepare($statement),
109 1
            $batch_size,
110 1
            $mappers    
111 1
        );
112
    }
113
114
    /**
115
     * @inheritdoc
116
     */
117 1
    public function execute(Statement $statement): PreparedStatement
118
    {
119 1
        $prepared_statement = $this->prepare($statement);
120
121 1
        $prepared_statement->execute();
122
        
123 1
        return $prepared_statement;
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129 1
    public function count(Countable $statement): int
130
    {
131 1
        return $this->fetch($statement->createCountStatement())->firstCol();
132
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137 1
    public function transact(callable $func): bool
138
    {
139 1
        if ($this->transaction_level === 0) {
140
            // starting a new stack of transactions - assume success:
141 1
            $this->pdo->beginTransaction();
142 1
            $this->transaction_result = true;
143
        }
144
145 1
        $this->transaction_level += 1;
146
147
        /**
148
         * @var mixed $commit return type of $func isn't guaranteed, therefore mixed rather than bool
149
         */
150 1
        $commit = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $commit is dead and can be removed.
Loading history...
151
152
        try {
153 1
            $commit = $func();
154 1
        } catch (Throwable $exception) {
155 1
            $commit = false;
156
        }
157
158 1
        $this->transaction_result = ($commit === true) && $this->transaction_result;
159
160 1
        $this->transaction_level -= 1;
161
162 1
        if ($this->transaction_level === 0) {
163 1
            if ($this->transaction_result === true) {
164 1
                $this->pdo->commit();
165
166 1
                return true; // the net transaction is a success!
167
            } else {
168 1
                $this->pdo->rollBack();
169
            }
170
        }
171
172 1
        if (isset($exception)) {
173
            // re-throw unhandled Exception:
174 1
            throw $exception;
175
        }
176
177 1
        if ($this->transaction_level > 0 && $commit === false) {
178 1
            throw new TransactionAbortedException("a nested call to transact() returned FALSE");
179
        }
180
181 1
        if (! is_bool($commit)) {
182 1
            throw new UnexpectedValueException("\$func must return TRUE (to commit) or FALSE (to roll back)");
183
        }
184
185 1
        return $this->transaction_result;
186
    }
187
188
    /**
189
     * Internally expand SQL placeholders (for array-types)
190
     *
191
     * @param string $sql SQL with placeholders
192
     * @param array<string|int|float|bool|null|array<string|int|float|bool|null>> $params placeholder name/value pairs
193
     *
194
     * @return string SQL with expanded placeholders
195
     */
196 1
    private function expandPlaceholders(string $sql, array $params): string
197
    {
198 1
        $replace_pairs = [];
199
200 1
        foreach ($params as $name => $value) {
201 1
            if (is_array($value)) {
202
                // TODO: QA! For empty arrays, the resulting SQL is e.g.: "SELECT * FROM foo WHERE foo.bar IN (null)"
203
204 1
                $replace_pairs[":{$name}"] = count($value) === 0
205 1
                    ? "(null)" // empty set
206 1
                    : "(" . implode(', ', array_map(function ($i) use ($name) {
207 1
                        return ":{$name}_{$i}";
208 1
                    }, range(1, count($value)))) . ")";
209
            }
210
        }
211
212 1
        return count($replace_pairs)
213 1
            ? strtr($sql, $replace_pairs)
214 1
            : $sql; // no arrays found in the given parameters
215
    }
216
217
    /**
218
     * @param $sequence_name auto-sequence name (or NULL for e.g. MySQL which supports only one auto-key)
0 ignored issues
show
Documentation Bug introduced by
The doc comment auto-sequence at position 0 could not be parsed: Unknown type name 'auto-sequence' at position 0 in auto-sequence.
Loading history...
219
     */
220
    public function lastInsertId(string|null $sequence_name = null): string|int|null
221
    {
222
        $id = $this->pdo->lastInsertId($sequence_name);
223
        
224
        if (is_numeric($id)) {
225
            return (int) $id;
226
        }
227
228
        if (is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
229
            return $id;
230
        }
231
        
232
        return null;
233
    }
234
    
235
    /**
236
     * @inheritdoc
237
     */
238 1
    public function addLogger(Logger $logger): void
239
    {
240 1
        $this->loggers[] = $logger;
241
    }
242
    
243
    /**
244
     * @inheritdoc
245
     */
246 1
    public function logQuery(string $sql, array $params, float $time_msec): void
247
    {
248 1
        foreach ($this->loggers as $logger) {
249 1
            $logger->logQuery($sql, $params, $time_msec);
250
        }
251
    }
252
}
253