Completed
Push — master ( 391062...ff47c5 )
by Rasmus
02:37
created

PDOConnection   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 94.55%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 169
wmc 21
lcom 1
cbo 4
ccs 52
cts 55
cp 0.9455
rs 10

7 Methods

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