Completed
Push — master ( af3a58...30eb3e )
by Rasmus
04:00
created

PDOConnection   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Test Coverage

Coverage 87.5%

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 7
dl 0
loc 232
ccs 70
cts 80
cp 0.875
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
B transact() 0 49 11
A expandPlaceholders() 0 20 5
A lastInsertId() 0 8 2
A addLogger() 0 4 1
A logQuery() 0 6 2
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 $error) {
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 $error) {
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)) {
0 ignored issues
show
Bug introduced by
The variable $exception seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
177
            // re-throw unhandled Exception:
178
            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