Completed
Push — master ( 7aab3a...2b8a1f )
by Vladimir
18:21 queued 01:15
created

Database::insert()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 8
cts 8
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 14
nc 3
nop 3
crap 4
1
<?php
2
/**
3
 * This file contains functionality related to interacting with the database this CMS uses
4
 *
5
 * @package    BZiON
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
use BZIon\Debug\DatabaseQuery;
10
use Monolog\Logger;
11
12
/**
13
 * Database interface class
14
 */
15
class Database
16
{
17
    /**
18
     * The global database connection object
19
     *
20
     * @todo Move this to the Service class
21
     * @var Database
22
     */
23
    private static $Database;
24
25
    /**
26
     * The database object used inside this class
27
     * @var PDO
28
     */
29
    private $dbc;
30
31
    /**
32
     * An instance of the logger
33
     * @var Logger
34
     */
35
    private $logger;
36
37
    /**
38
     * The id of the last row entered
39
     * @var int
40
     */
41
    private $last_id;
42
43
    /**
44
     * Create a new connection to the database
45
     *
46
     * @param string $host     The MySQL host
47
     * @param string $user     The MySQL user
48
     * @param string $password The MySQL password for the user
49
     * @param string $dbName   The MySQL database name
50
     */
51 1
    public function __construct($host, $user, $password, $dbName)
52
    {
53 1
        if (Service::getContainer()) {
54 1
            if ($logger = Service::getContainer()->get('monolog.logger.mysql')) {
55 1
                $this->logger = $logger;
56
            }
57
        }
58
59
        try {
60
            // TODO: Persist
61 1
            $this->dbc = new PDO(
62 1
                'mysql:host=' . $host . ';dbname=' . $dbName . ';charset=utf8',
63 1
                $user,
64 1
                $password,
65
                array(
66 1
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
67
68
                    // We are using MySQL, so there is no need to emulate
69
                    // prepared statements for databases that don't support
70
                    // them. This line makes sure all values are returned to PHP
71
                    // from MySQL in the correct type, and they are not all
72
                    // strings.
73
                    PDO::ATTR_EMULATE_PREPARES => false
74
                )
75
            );
76
        } catch (PDOException $e) {
77
            $this->logger->addAlert($e->getMessage());
78
            throw new Exception($e->getMessage(), $e->getCode());
79
        }
80 1
    }
81
82
    /**
83
     * Destroy this connection to the database
84
     */
85
    public function __destruct()
86
    {
87
        $this->closeConnection();
88
    }
89
90
    /**
91
     * Get an instance of the Database object
92
     *
93
     * This should be the main way to acquire access to the database
94
     *
95
     * @todo Move this to the Service class
96
     *
97
     * @throws \Exception When no testing environment has been specified in the configuation file.
98
     *
99 76
     * @return Database The Database object
100
     */
101 76
    public static function getInstance()
102 1
    {
103 1
        if (!self::$Database) {
104
            if (Service::getEnvironment() == 'test') {
105
                if (!Service::getParameter('bzion.testing.enabled')) {
106
                    throw new Exception('You have to specify a MySQL database for testing in the bzion.testing section of your configuration file.');
107 1
                }
108 1
109 1
                self::$Database = new self(
110 1
                    Service::getParameter('bzion.testing.host'),
111 1
                    Service::getParameter('bzion.testing.username'),
112
                    Service::getParameter('bzion.testing.password'),
113
                    Service::getParameter('bzion.testing.database')
114
                );
115
            } else {
116
                self::$Database = new self(
117
                    Service::getParameter('bzion.mysql.host'),
118
                    Service::getParameter('bzion.mysql.username'),
119
                    Service::getParameter('bzion.mysql.password'),
120
                    Service::getParameter('bzion.mysql.database')
121
                );
122
            }
123 76
        }
124
125
        return self::$Database;
126
    }
127
128
    /**
129
     * Close the current connection to the MySQL database
130
     */
131
    public function closeConnection()
132
    {
133
        $this->dbc = null;
134
    }
135
136
    /**
137
     * Tests whether or not the connection to the database is still active
138
     * @todo Make this work for PDO, or deprecate it if not needed
139
     * @return bool True if the connection is active
140
     */
141
    public function isConnected()
142
    {
143
        return true;
144
    }
145
146
    /**
147
     * Get the unique row ID of the last row that was inserted
148 76
     * @return int The ID of the row
149
     */
150 76
    public function getInsertId()
151
    {
152
        return $this->last_id;
153
    }
154
155
    /**
156
     * Insert an associative array into the database.
157
     *
158
     * @param string            $table  The table to perform the query on
159
     * @param array             $params An associative array, with the keys (columns) pointing to the values you want to put on each
160 76
     * @param array|string|null $now    Column(s) to update with the current timestamp
161
     *
162 76
     * @return int
163 76
     */
164
    public function insert($table, array $params, $now = null)
165
    {
166 76
        $columns = implode('`,`', array_keys($params));
167
        $columns = "`$columns`";
168 76
169 76
        $question_marks = str_repeat('?,', count($params));
170
        $question_marks = rtrim($question_marks, ','); // Remove last comma
171 76
172
        if ($now) {
173 76
            if (!is_array($now)) {
174
                // Convert $now to an array if it's a string
175
                $now = array($now);
176
            }
177
178
            foreach ($now as $column) {
179
                $columns .= ",$column";
180
                $question_marks .= ",UTC_TIMESTAMP()";
181
            }
182
        }
183 76
184
        $query = "INSERT INTO $table ($columns) VALUES ($question_marks)";
185 76
        $this->execute($query, array_values($params));
186 1
187
        return $this->getInsertId();
188
    }
189 76
190
    /**
191 76
     * Insert an array of associative arrays into the database.
192
     *
193 76
     * @param string            $table The table to perform the query on
194
     * @param array             $batch An array of associative arrays that'll be passed on to Database::insert()
195 76
     * @param array|string|null $now   Column(s) to update with the current timestamp
196
     */
197
    public function insertBatch($table, array $batch, $now = null)
198
    {
199
        $this->startTransaction();
200
201
        foreach ($batch as $entry) {
202
            $this->insert($table, $entry, $now);
203
        }
204
205 76
        $this->commit();
206
    }
207
208 76
    /**
209
     * Prepares and executes a MySQL prepared INSERT/DELETE/UPDATE statement. <em>The second parameter is optional when using this function to execute a query with no placeholders.</em>
210 76
     *
211 76
     * @param  string      $queryText The prepared SQL statement that will be executed
212 76
     * @param  mixed|array $params    (Optional) The array of values that will be binded to the prepared statement
213
     * @return int         Returns the number of rows affected
214 76
     */
215 76 View Code Duplication
    public function execute($queryText, $params = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216 76
    {
217 76
        if (!is_array($params)) {
218 76
            $params = array($params);
219 76
        }
220 76
221 76
        $debug = new DatabaseQuery($queryText, $params);
222
223
        $query = $this->doQuery($queryText, $params);
224
        $return = $query->rowCount();
225 76
226
        $debug->finish($return);
227
228 76
        return $return;
229
    }
230
231 76
    /**
232
     * Prepares and executes a MySQL prepared SELECT statement. <em>The second parameter is optional when using this function to execute a query with no placeholders.</em>
233
     *
234
     * @param  string      $queryText The prepared SQL statement that will be executed
235
     * @param  mixed|array $params    (Optional) The array of values that will be binded to the prepared statement
236 76
     * @return array       Returns an array of the values received from the query
237 76
     */
238 View Code Duplication
    public function query($queryText, $params = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
239
    {
240
        if (!is_array($params)) {
241 76
            $params = array($params);
242
        }
243 76
244 1
        $debug = new DatabaseQuery($queryText, $params);
245 1
246
        $return = $this->doQuery($queryText, $params)->fetchAll();
247
248
        $debug->finish($return);
249
250
        return $return;
251
    }
252 2
253
    /**
254 2
     * Perform a query
255 2
     * @param  string      $queryText The prepared SQL statement that will be executed
256
     * @param  null|array  $params    (Optional) The array of values that will be binded to the prepared statement
257
     *
258
     * @return PDOStatement The PDO statement
259
     */
260
    private function doQuery($queryText, $params = null)
261
    {
262
        try {
263
            $query = $this->dbc->prepare($queryText);
264
265
            if ($params !== null) {
266
                $i = 1;
267
                foreach ($params as $name => $param) {
268
                    // Guess parameter type
269
                    if (is_bool($param)) {
270
                        $param = (int) $param;
271
                        $type = PDO::PARAM_INT;
272
                    } elseif (is_int($param)) {
273
                        $type = PDO::PARAM_INT;
274
                    } elseif (is_null($param)) {
275
                        $type = PDO::PARAM_NULL;
276
                    } elseif ($param instanceof ModelInterface) {
277
                        $param = (int) $param->getId();
278 2
                        $type = PDO::PARAM_INT;
279
                    } else {
280 2
                        $type = PDO::PARAM_STR;
281 2
                    }
282
283
                    if (is_string($name)) {
284
                        $query->bindValue($name, $param, $type);
285
                    } else {
286
                        $query->bindValue($i++, $param, $type);
287
                    }
288
                }
289
            }
290
291
            $result = $query->execute();
292 1
            if ($result === false) {
293
                $this->error("Unknown error");
294 1
            }
295
296
            $this->last_id = $this->dbc->lastInsertId();
0 ignored issues
show
Documentation Bug introduced by
The property $last_id was declared of type integer, but $this->dbc->lastInsertId() is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
297
298
            return $query;
299 1
        } catch (PDOException $e) {
300 1
            $this->error($e->getMessage(), $e->getCode(), $e);
301 1
        }
302
    }
303
304 1
    /**
305 1
     * Start a MySQL transaction
306
     */
307
    public function startTransaction()
308
    {
309
        $this->dbc->beginTransaction();
310
    }
311
312
    /**
313
     * Commit the stored queries (usable only if a transaction has been started)
314
     *
315
     * This does not show an error if there are no queries to commit
316
     */
317
    public function commit()
318
    {
319
        $this->dbc->commit();
320
    }
321
322
    /**
323
     * Cancel all pending queries (does not finish the transaction
324
     */
325
    public function rollback()
326
    {
327
        $this->dbc->rollBack();
328
    }
329
330
    /**
331
     * Commit all pending queries and finalise the transaction
332
     */
333
    public function finishTransaction()
334
    {
335
        $this->dbc->commit();
336
    }
337
338
    /**
339
     * Uses monolog to log an error message
340
     *
341
     * @param string         $error    The error string
342
     * @param int            $id       The error ID
343
     * @param Exception|null $previous The exception that caused the error (if any)
344
     *
345
     * @todo When PHP 5.x support is dropped, replace Exception with Throwable
346
     *
347
     * @throws Exception
348
     */
349
    public function error($error, $id = null, Exception $previous = null)
350
    {
351
        if (empty($error)) {
352
            $error = "Unknown MySQL error - check for warnings generated by PHP";
353
        }
354
355
        // Create a context array so that we can log the ID, if provided
356
        $context = array();
357
        if ($id !== null) {
358
            $context['id'] = $id;
359
        }
360
361
        $this->logger->addError($error, $context);
362
        throw new Exception($error, (int) $id, $previous);
363
    }
364
365
    /**
366
     * Serialize the object
367
     *
368
     * Prevents PDO from being erroneously serialized
369
     *
370
     * @return array The list of properties that should be serialized
371
     */
372
    public function __sleep()
373
    {
374
        return array('last_id');
375
    }
376
}
377