Completed
Push — master ( 471540...e3dcd8 )
by Rasmus
03:02
created

PDOConnection   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 86.36%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
wmc 26
c 4
b 0
f 1
lcom 1
cbo 5
dl 0
loc 200
ccs 57
cts 66
cp 0.8636
rs 10

8 Methods

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