Completed
Push — master ( a1178b...339ab1 )
by Rasmus
03:22
created

PDOConnection::logQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
231
    {
232 1
        foreach ($this->loggers as $logger) {
233 1
            $logger->logQuery($sql, $params, $time_msec);
234
        }
235 1
    }
236
237
}
238