Completed
Push — master ( f5e696...7c0e7c )
by Rasmus
03:36
created

PDOConnection::getPDO()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 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
     * @return PDO the internal PDO connection object
66
     */
67
    public function getPDO()
68
    {
69
        return $this->pdo;
70
    }
71
72
    /**
73
     * @inheritdoc
74
     */
75 1
    public function prepare(Statement $statement)
76
    {
77 1
        $params = $statement->getParams();
78
79 1
        $sql = $this->expandPlaceholders($statement->getSQL(), $params);
80
81 1
        $prepared_statement = new PreparedPDOStatement($this->pdo->prepare($sql), $this, $this->types, $this);
82
        
83 1
        foreach ($params as $name => $value) {
84 1
            if (is_array($value)) {
85 1
                $index = 1; // use a base-1 offset consistent with expandPlaceholders()
86
87 1
                foreach ($value as $item) {
88
                    // NOTE: we deliberately ignore the array indices here, as using them could result in broken SQL!
89
90 1
                    $prepared_statement->bind("{$name}_{$index}", $item);
91
92 1
                    $index += 1;
93
                }
94
            } else {
95 1
                $prepared_statement->bind($name, $value);
96
            }
97
        }
98
99 1
        return $prepared_statement;
100
    }
101
102
    /**
103
     * @inheritdoc
104
     */
105 1
    public function fetch(Statement $statement, $batch_size = 1000)
106
    {
107 1
        $mappers = $statement instanceof MapperProvider
108 1
            ? $statement->getMappers()
109 1
            : [];
110
        
111 1
        return new Result(
112 1
            $this->prepare($statement),
113
            $batch_size,
114 1
            $mappers    
115
        );
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121 1
    public function execute(Statement $statement)
122
    {
123 1
        $prepared_statement = $this->prepare($statement);
124
125 1
        $prepared_statement->execute();
126
        
127 1
        return $prepared_statement;
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133 1
    public function count(Countable $statement)
134
    {
135 1
        return $this->fetch($statement->createCountStatement())->firstCol();
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141 1
    public function transact(callable $func)
142
    {
143 1
        if ($this->transaction_level === 0) {
144
            // starting a new stack of transactions - assume success:
145 1
            $this->pdo->beginTransaction();
146 1
            $this->transaction_result = true;
147
        }
148
149 1
        $this->transaction_level += 1;
150
151
        /** @var mixed $commit return type of $func isn't guaranteed, therefore mixed rather than bool */
152
153
        try {
154 1
            $commit = call_user_func($func);
155 1
        } catch (Exception $exception) {
156 1
            $commit = false;
157
        }
158
159 1
        $this->transaction_result = ($commit === true) && $this->transaction_result;
160
161 1
        $this->transaction_level -= 1;
162
163 1
        if ($this->transaction_level === 0) {
164 1
            if ($this->transaction_result === true) {
165 1
                $this->pdo->commit();
166
167 1
                return true; // the net transaction is a success!
168
            } else {
169 1
                $this->pdo->rollBack();
170
            }
171
        }
172
173 1
        if (isset($exception)) {
174
            // re-throw unhandled Exception:
175 1
            throw $exception;
176
        }
177
178 1
        if ($this->transaction_level > 0 && $commit === false) {
179 1
            throw new TransactionAbortedException("a nested call to transact() returned FALSE");
180
        }
181
182 1
        if (! is_bool($commit)) {
183 1
            throw new UnexpectedValueException("\$func must return TRUE (to commit) or FALSE (to roll back)");
184
        }
185
186 1
        return $this->transaction_result;
187
    }
188
189
    /**
190
     * Internally expand SQL placeholders (for array-types)
191
     *
192
     * @param string $sql    SQL with placeholders
193
     * @param array  $params placeholder name/value pairs
194
     *
195
     * @return string SQL with expanded placeholders
196
     */
197 1
    private function expandPlaceholders($sql, array $params)
198
    {
199 1
        $replace_pairs = [];
200
201 1
        foreach ($params as $name => $value) {
202 1
            if (is_array($value)) {
203
                // TODO: QA! For empty arrays, the resulting SQL is e.g.: "SELECT * FROM foo WHERE foo.bar IN (null)"
204
205 1
                $replace_pairs[":{$name}"] = count($value) === 0
206 1
                    ? "(null)" // empty set
207 1
                    : "(" . implode(', ', array_map(function ($i) use ($name) {
208 1
                        return ":{$name}_{$i}";
209 1
                    }, range(1, count($value)))) . ")";
210
            }
211
        }
212
213 1
        return count($replace_pairs)
214 1
            ? strtr($sql, $replace_pairs)
215 1
            : $sql; // no arrays found in the given parameters
216
    }
217
218
    /**
219
     * @param string|null $sequence_name auto-sequence name (or NULL for e.g. MySQL which supports only one auto-key)
220
     *
221
     * @return int|string
222
     */
223
    public function lastInsertId($sequence_name = null)
224
    {
225
        $id = $this->pdo->lastInsertId($sequence_name);
226
        
227
        return is_numeric($id)
228
            ? (int) $id
229
            : $id;
230
    }
231
    
232
    /**
233
     * @inheritdoc
234
     */
235 1
    public function addLogger(Logger $logger)
236
    {
237 1
        $this->loggers[] = $logger;
238 1
    }
239
    
240
    /**
241
     * @inheritdoc
242
     */
243 1
    public function logQuery($sql, $params, $time_msec)
244
    {
245 1
        foreach ($this->loggers as $logger) {
246 1
            $logger->logQuery($sql, $params, $time_msec);
247
        }
248 1
    }
249
}
250