Completed
Push — master ( 30eb3e...da51da )
by Rasmus
03:03 queued 01:12
created

PDOConnection   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Test Coverage

Coverage 88.75%

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 7
dl 0
loc 232
ccs 71
cts 80
cp 0.8875
rs 9.92
c 0
b 0
f 0

11 Methods

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