Completed
Push — master ( f707f5...a08928 )
by Rasmus
02:27
created

PDOConnection::lastInsertId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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