Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
7.07% |
7 / 99 |
|
20.00% |
3 / 15 |
CRAP | |
0.00% |
0 / 1 |
AbstractDao | |
7.07% |
7 / 99 |
|
20.00% |
3 / 15 |
906.95 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
transactionStart | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
transactionCommit | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
transactionRollback | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
bind | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
90 | |||
fetch | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
fetchAll | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
fetchAllWithFound | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
update | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
insert | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
buildWhere | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildHaving | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildBooleanClause | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
concat | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
makeBindParams | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * @section LICENSE |
4 | * This file is part of Wikimedia Slim application library |
5 | * |
6 | * Wikimedia Slim application library is free software: you can |
7 | * redistribute it and/or modify it under the terms of the GNU General Public |
8 | * License as published by the Free Software Foundation, either version 3 of |
9 | * the License, or (at your option) any later version. |
10 | * |
11 | * Wikimedia Slim application library is distributed in the hope that it |
12 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty |
13 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with Wikimedia Grants Review application. If not, see |
18 | * <http://www.gnu.org/licenses/>. |
19 | * |
20 | * @file |
21 | * @copyright © 2015 Bryan Davis, Wikimedia Foundation and contributors. |
22 | */ |
23 | |
24 | namespace Wikimedia\Slimapp\Dao; |
25 | |
26 | use PDO; |
27 | use PDOException; |
28 | use PDOStatement; |
29 | use Psr\Log\LoggerInterface; |
30 | use Psr\Log\NullLogger; |
31 | use stdClass; |
32 | |
33 | /** |
34 | * Base class for data access objects. |
35 | * |
36 | * @author Bryan Davis <bd808@wikimedia.org> |
37 | * @copyright © 2015 Bryan Davis, Wikimedia Foundation and contributors. |
38 | */ |
39 | abstract class AbstractDao { |
40 | |
41 | /** |
42 | * @var PDO |
43 | */ |
44 | protected $dbh; |
45 | |
46 | /** |
47 | * @var LoggerInterface |
48 | */ |
49 | protected $logger; |
50 | |
51 | /** |
52 | * @var int Used for keeping track of transaction status |
53 | */ |
54 | protected $transactionCounter = 0; |
55 | |
56 | /** |
57 | * @param string $dsn PDO data source name |
58 | * @param string $user Database user |
59 | * @param string $pass Database password |
60 | * @param LoggerInterface $logger Log channel |
61 | */ |
62 | public function __construct( $dsn, $user, $pass, $logger = null ) { |
63 | $this->logger = $logger ?: new NullLogger(); |
64 | |
65 | $this->dbh = new PDO( $dsn, $user, $pass, |
66 | [ |
67 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, |
68 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, |
69 | ] |
70 | ); |
71 | } |
72 | |
73 | /** |
74 | * Start a new transaction |
75 | * |
76 | * If already a transaction has been started, it will only increment the |
77 | * counter. This method is useful in nested transactions. |
78 | * @return bool True on success, false on failure. |
79 | */ |
80 | protected function transactionStart() { |
81 | if ( $this->transactionCounter === 0 ) { |
82 | $this->transactionCounter++; |
83 | return $this->dbh->beginTransaction(); |
84 | } |
85 | $this->transactionCounter++; |
86 | return $this->transactionCounter >= 0; |
87 | } |
88 | |
89 | /** |
90 | * Commit a transaction |
91 | * |
92 | * If the transaction counter is zero, commit the transaction otherwise |
93 | * decrement the transaction counter. This method is useful in nested |
94 | * transactions. |
95 | * @return bool True on success, false on failure. |
96 | */ |
97 | protected function transactionCommit() { |
98 | $this->transactionCounter--; |
99 | if ( $this->transactionCounter === 0 ) { |
100 | return $this->dbh->commit(); |
101 | } |
102 | return $this->transactionCounter >= 0; |
103 | } |
104 | |
105 | /** |
106 | * Rollback a transaction |
107 | * |
108 | * If the transaction counter is greater than 0, set it to |
109 | * 0 and rollback the transaction. This method is useful in nested |
110 | * transactions. |
111 | * @return bool True on success, false on failure. |
112 | */ |
113 | protected function transactionRollback() { |
114 | if ( $this->transactionCounter >= 0 ) { |
115 | $this->transactionCounter = 0; |
116 | return $this->dbh->rollback(); |
117 | } |
118 | $this->transactionCounter = 0; |
119 | return false; |
120 | } |
121 | |
122 | /** |
123 | * Bind values to a prepared statement. |
124 | * |
125 | * If an associative array of values is provided, the data type to use when |
126 | * binding will be inferred by looking for a "<type>_" prefix at the |
127 | * beginning of the array key. This can come in very handy if you are using |
128 | * parameters in places like LIMIT clauses where binding as a string (the |
129 | * default type for PDO binds) will cause a syntax error. |
130 | * |
131 | * @param PDOStatement $stmt Previously prepared statement |
132 | * @param array $values Values to bind |
133 | */ |
134 | protected function bind( $stmt, $values ) { |
135 | $values = $values ?: []; |
136 | |
137 | if ( count( array_filter( array_keys( $values ), 'is_string' ) ) ) { |
138 | // associative array provided |
139 | foreach ( $values as $key => $value ) { |
140 | // infer bind type from key prefix |
141 | [ $prefix, ] = explode( '_', "{$key}_", 2 ); |
142 | |
143 | switch ( $prefix ) { |
144 | case 'int': |
145 | $type = PDO::PARAM_INT; |
146 | break; |
147 | case 'bool': |
148 | $type = PDO::PARAM_BOOL; |
149 | break; |
150 | case 'null': |
151 | $type = PDO::PARAM_NULL; |
152 | break; |
153 | default: |
154 | $type = PDO::PARAM_STR; |
155 | } |
156 | |
157 | $stmt->bindValue( $key, $value, $type ); |
158 | } |
159 | |
160 | } else { |
161 | // vector provided |
162 | $idx = 1; |
163 | foreach ( $values as $value ) { |
164 | $stmt->bindValue( $idx, $value ); |
165 | $idx++; |
166 | } |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Prepare and execute an SQL statement and return the first row of results. |
172 | * |
173 | * @param string $sql SQL |
174 | * @param array $params Prepared statement parameters |
175 | * @return array First response row |
176 | */ |
177 | protected function fetch( $sql, $params = null ) { |
178 | $stmt = $this->dbh->prepare( $sql ); |
179 | $this->bind( $stmt, $params ); |
180 | $stmt->execute(); |
181 | return $stmt->fetch(); |
182 | } |
183 | |
184 | /** |
185 | * Prepare and execute an SQL statement and return all results. |
186 | * |
187 | * @param string $sql SQL |
188 | * @param array $params Prepared statement parameters |
189 | * @return array Result rows |
190 | */ |
191 | protected function fetchAll( $sql, $params = null ) { |
192 | $this->logger->debug( $sql, $params ?: [] ); |
193 | $stmt = $this->dbh->prepare( $sql ); |
194 | $this->bind( $stmt, $params ); |
195 | $stmt->execute(); |
196 | return $stmt->fetchAll(); |
197 | } |
198 | |
199 | /** |
200 | * Prepare and execute an SQL statement and return all results plus the |
201 | * number of rows found on the server side. |
202 | * |
203 | * The SQL is expected to contain the "SQL_CALC_FOUND_ROWS" option in the |
204 | * select statement. If it does not, the number of found rows returned is |
205 | * dependent on MySQL's interpretation of the query. |
206 | * |
207 | * @param string $sql SQL |
208 | * @param array $params Prepared statement parameters |
209 | * @return stdClass StdClass with rows and found members |
210 | */ |
211 | protected function fetchAllWithFound( $sql, $params = null ) { |
212 | $ret = new stdClass; |
213 | $ret->rows = $this->fetchAll( $sql, $params ); |
214 | |
215 | $ret->found = $this->fetch( 'SELECT FOUND_ROWS() AS found' ); |
216 | $ret->found = $ret->found['found']; |
217 | |
218 | return $ret; |
219 | } |
220 | |
221 | /** |
222 | * Prepare and execute an SQL statement in a transaction. |
223 | * |
224 | * @param string $sql SQL |
225 | * @param array $params Prepared statement parameters |
226 | * @return bool False if an exception was generated, true otherwise |
227 | */ |
228 | protected function update( $sql, $params = null ) { |
229 | $stmt = $this->dbh->prepare( $sql ); |
230 | try { |
231 | $this->transactionStart(); |
232 | $stmt->execute( $params ); |
233 | $this->transactionCommit(); |
234 | return true; |
235 | |
236 | } catch ( PDOException $e ) { |
237 | $this->transactionRollback(); |
238 | $this->logger->error( 'Update failed.', [ |
239 | 'method' => __METHOD__, |
240 | 'exception' => $e, |
241 | 'sql' => $sql, |
242 | 'params' => $params, |
243 | ] ); |
244 | return false; |
245 | } |
246 | } |
247 | |
248 | /** |
249 | * Prepare and execute an SQL statement in a transaction. |
250 | * |
251 | * @param string $sql SQL |
252 | * @param array|null $params Prepared statement parameters |
253 | * @return string|false Last insert id or false if an exception was generated |
254 | */ |
255 | protected function insert( $sql, $params = null ) { |
256 | $stmt = $this->dbh->prepare( $sql ); |
257 | try { |
258 | $this->transactionStart(); |
259 | $stmt->execute( $params ); |
260 | $rowid = $this->dbh->lastInsertId(); |
261 | $this->transactionCommit(); |
262 | return $rowid; |
263 | |
264 | } catch ( PDOException $e ) { |
265 | $this->transactionRollback(); |
266 | $this->logger->error( 'Insert failed.', [ |
267 | 'method' => __METHOD__, |
268 | 'exception' => $e, |
269 | 'sql' => $sql, |
270 | 'params' => $params, |
271 | ] ); |
272 | return false; |
273 | } |
274 | } |
275 | |
276 | /** |
277 | * Construct a where clause. |
278 | * @param array $where List of conditions |
279 | * @param string $conjunction Joining operation ('and' or 'or') |
280 | * @return string Where clause or empty string |
281 | */ |
282 | protected static function buildWhere( array $where, $conjunction = 'AND' ) { |
283 | return static::buildBooleanClause( 'WHERE', $where, $conjunction ); |
284 | } |
285 | |
286 | /** |
287 | * Construct a having clause. |
288 | * @param array $having List of conditions |
289 | * @param string $conjunction Joining operation ('and' or 'or') |
290 | * @return string Having clause or empty string |
291 | */ |
292 | protected static function buildHaving( |
293 | array $having, $conjunction = 'AND' |
294 | ) { |
295 | return static::buildBooleanClause( 'HAVING', $having, $conjunction ); |
296 | } |
297 | |
298 | /** |
299 | * Construct a boolean clause. |
300 | * @param string $type Clause type (eg 'WHERE', 'HAVING') |
301 | * @param array $expressions List of expressions |
302 | * @param string $conjunction Joining operation ('AND' or 'OR') |
303 | * @return string Clause or empty string |
304 | */ |
305 | protected static function buildBooleanClause( |
306 | $type, array $expressions, $conjunction = 'AND' |
307 | ) { |
308 | if ( $expressions ) { |
309 | return "{$type} (" . |
310 | implode( ") {$conjunction} (", $expressions ) . |
311 | ') '; |
312 | } |
313 | return ''; |
314 | } |
315 | |
316 | /** |
317 | * Create a string by joining all arguments with spaces. |
318 | * |
319 | * If one or more of the arguments are arrays each element of the array will |
320 | * be included independently. |
321 | * |
322 | * @return string New string |
323 | */ |
324 | protected static function concat( /*varags*/ ) { |
325 | $args = []; |
326 | foreach ( func_get_args() as $arg ) { |
327 | if ( is_array( $arg ) ) { |
328 | $args = array_merge( $args, $arg ); |
329 | } else { |
330 | $args[] = $arg; |
331 | } |
332 | } |
333 | |
334 | return implode( ' ', $args ); |
335 | } |
336 | |
337 | /** |
338 | * Create a list of bind parameters from a list of strings. |
339 | * |
340 | * @param array $list List of strings to convert to bind parameters |
341 | * @return array List of bind parameters (eg ':field1) |
342 | */ |
343 | protected static function makeBindParams( array $list ) { |
344 | return array_map( static function ( $elm ) { |
345 | return ":{$elm}"; |
346 | }, $list ); |
347 | } |
348 | } |