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