Completed
Push — master ( 18d42b...fed0be )
by Rasmus
03:18
created

PDOConnection   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 180
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 85.25%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 22
c 5
b 0
f 0
lcom 1
cbo 4
dl 0
loc 180
ccs 52
cts 61
cp 0.8525
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B prepare() 0 26 4
A fetch() 0 8 1
A execute() 0 8 1
C transact() 0 43 8
B expandPlaceholders() 0 20 5
A lastInsertId() 0 8 2
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\ReturningExecutable;
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(ReturningExecutable $statement, $batch_size = 1000, array $mappers = [])
88
    {
89 1
        return new Result(
90 1
            $this->prepare($statement),
91
            $batch_size,
92 1
            array_merge($statement->getMappers(), $mappers)
93
        );
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public function execute(Executable $statement)
100
    {
101
        $prepared_statement = $this->prepare($statement);
102
103
        $prepared_statement->execute();
104
        
105
        return $prepared_statement;
106
    }
107
108
    /**
109
     * @inheritdoc
110
     */
111 1
    public function transact(callable $func)
112
    {
113 1
        if ($this->transaction_level === 0) {
114
            // starting a new stack of transactions - assume success:
115 1
            $this->pdo->beginTransaction();
116 1
            $this->transaction_result = true;
117
        }
118
119 1
        $this->transaction_level += 1;
120
121
        /** @var mixed $commit return type of $func isn't guaranteed, therefore mixed rather than bool */
122
123
        try {
124 1
            $commit = call_user_func($func);
125 1
        } catch (Exception $exception) {
126 1
            $commit = false;
127
        }
128
129 1
        $this->transaction_result = ($commit === true) && $this->transaction_result;
130
131 1
        $this->transaction_level -= 1;
132
133 1
        if ($this->transaction_level === 0) {
134 1
            if ($this->transaction_result === true) {
135 1
                $this->pdo->commit();
136
137 1
                return true; // the net transaction is a success!
138
            } else {
139 1
                $this->pdo->rollBack();
140
            }
141
        }
142
143 1
        if (isset($exception)) {
144
            // re-throw unhandled Exception as a LogicException:
145 1
            throw new LogicException("unhandled Exception during transaction", 0, $exception);
146
        }
147
148 1
        if (! is_bool($commit)) {
149 1
            throw new UnexpectedValueException("\$func must return TRUE (to commit) or FALSE (to roll back)");
150
        }
151
152 1
        return $this->transaction_result;
153
    }
154
155
    /**
156
     * Internally expand SQL placeholders (for array-types)
157
     *
158
     * @param string $sql    SQL with placeholders
159
     * @param array  $params placeholder name/value pairs
160
     *
161
     * @return string SQL with expanded placeholders
162
     */
163 1
    private function expandPlaceholders($sql, array $params)
164
    {
165 1
        $replace_pairs = [];
166
167 1
        foreach ($params as $name => $value) {
168 1
            if (is_array($value)) {
169
                // TODO: QA! For empty arrays, the resulting SQL is e.g.: "SELECT * FROM foo WHERE foo.bar IN (null)"
170
171 1
                $replace_pairs[":{$name}"] = count($value) === 0
172 1
                    ? "(null)" // empty set
173 1
                    : "(" . implode(', ', array_map(function ($i) use ($name) {
174 1
                        return ":{$name}_{$i}";
175 1
                    }, range(1, count($value)))) . ")";
176
            }
177
        }
178
179 1
        return count($replace_pairs)
180 1
            ? strtr($sql, $replace_pairs)
181 1
            : $sql; // no arrays found in the given parameters
182
    }
183
184
    /**
185
     * @param string|null $sequence_name auto-sequence name (or NULL for e.g. MySQL which supports only one auto-key)
186
     *
187
     * @return int|string
188
     */
189
    public function lastInsertId($sequence_name = null)
190
    {
191
        $id = $this->pdo->lastInsertId($sequence_name);
192
        
193
        return is_numeric($id)
194
            ? (int) $id
195
            : $id;
196
    }
197
}
198