Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
7.07% covered (danger)
7.07%
7 / 99
20.00% covered (danger)
20.00%
3 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractDao
7.07% covered (danger)
7.07%
7 / 99
20.00% covered (danger)
20.00%
3 / 15
906.95
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 transactionStart
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 transactionCommit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 transactionRollback
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 bind
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 fetch
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 fetchAll
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 fetchAllWithFound
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 insert
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 buildWhere
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildHaving
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildBooleanClause
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 concat
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 makeBindParams
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
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
24namespace Wikimedia\Slimapp\Dao;
25
26use PDO;
27use PDOException;
28use PDOStatement;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use 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 */
39abstract 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}