Completed
Push — master ( 79699f...13fedd )
by Rasmus
02:36
created

PDOConnection   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Test Coverage

Coverage 93.24%

Importance

Changes 7
Bugs 0 Features 2
Metric Value
c 7
b 0
f 2
dl 0
loc 222
wmc 29
lcom 2
cbo 6
ccs 69
cts 74
cp 0.9324
rs 10

10 Methods

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