Completed
Push — master ( 2b8686...f707f5 )
by Rasmus
02:35
created

PDOConnection::expandPlaceholders()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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