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