Completed
Pull Request — master (#34)
by Rasmus
11:05
created

PDOConnection::prepare()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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