Completed
Pull Request — master (#51)
by Konstantinos
08:32 queued 04:30
created

Database::error()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 15
ccs 5
cts 6
cp 0.8333
rs 9.4285
cc 3
eloc 8
nc 4
nop 3
crap 3.0416
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
                $user,
64
                $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
     * @return Database The Database object
98
     */
99 39
    public static function getInstance()
100
    {
101 39
        if (!self::$Database) {
102 1
            if (Service::getEnvironment() == 'test') {
103 1
                if (!Service::getParameter('bzion.testing.enabled')) {
104
                    throw new Exception('You have to specify a MySQL database for testing in the bzion.testing section of your configuration file.');
105
                }
106
107 1
                self::$Database = new self(
108 1
                    Service::getParameter('bzion.testing.host'),
109 1
                    Service::getParameter('bzion.testing.username'),
110 1
                    Service::getParameter('bzion.testing.password'),
111 1
                    Service::getParameter('bzion.testing.database')
112
                );
113
            } else {
114
                self::$Database = new self(
115
                    Service::getParameter('bzion.mysql.host'),
116
                    Service::getParameter('bzion.mysql.username'),
117
                    Service::getParameter('bzion.mysql.password'),
118
                    Service::getParameter('bzion.mysql.database')
119
                );
120
            }
121
        }
122
123 39
        return self::$Database;
124
    }
125
126
    /**
127
     * Close the current connection to the MySQL database
128
     */
129
    public function closeConnection()
130
    {
131
        $this->dbc = null;
132
    }
133
134
    /**
135
     * Tests whether or not the connection to the database is still active
136
     * @todo Make this work for PDO, or deprecate it if not needed
137
     * @return bool True if the connection is active
138
     */
139
    public function isConnected()
140
    {
141
        return true;
142
    }
143
144
    /**
145
     * Get the unique row ID of the last row that was inserted
146
     * @return int The ID of the row
147
     */
148 39
    public function getInsertId()
149
    {
150 39
        return $this->last_id;
151
    }
152
153
    /**
154
     * 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>
155
     *
156
     * @param  string      $queryText The prepared SQL statement that will be executed
157
     * @param  mixed|array $params    (Optional) The array of values that will be binded to the prepared statement
158
     * @return array       Returns an array of the values received from the query
159
     */
160 39 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...
161
    {
162 39
        if (!is_array($params)) {
163 39
            $params = array($params);
164
        }
165
166 39
        $debug = new DatabaseQuery($queryText, $params);
167
168 39
        $query = $this->doQuery($queryText, $params);
169 39
        $return = $query->rowCount();
170
171 39
        $debug->finish($return);
172
173 39
        return $return;
174
    }
175
176
    /**
177
     * 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>
178
     *
179
     * @param  string      $queryText The prepared SQL statement that will be executed
180
     * @param  mixed|array $params    (Optional) The array of values that will be binded to the prepared statement
181
     * @return array       Returns an array of the values received from the query
182
     */
183 39 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...
184
    {
185 39
        if (!is_array($params)) {
186 1
            $params = array($params);
187
        }
188
189 39
        $debug = new DatabaseQuery($queryText, $params);
190
191 39
        $return = $this->doQuery($queryText, $params)->fetchAll();
192
193 39
        $debug->finish($return);
194
195 39
        return $return;
196
    }
197
198
    /**
199
     * Perform a query
200
     * @param  string      $queryText The prepared SQL statement that will be executed
201
     * @param  null|array  $params    (Optional) The array of values that will be binded to the prepared statement
202
     *
203
     * @return PDOStatement The PDO statement
204
     */
205 39
    private function doQuery($queryText, $params = null)
206
    {
207
        try {
208 39
            $query = $this->dbc->prepare($queryText);
209
210 39
            if ($params !== null) {
211 39
                $i = 1;
212 39
                foreach ($params as $name => $param) {
213
                    // Guess parameter type
214 39
                    if (is_bool($param)) {
215 39
                        $param = (int) $param;
216 39
                        $type = PDO::PARAM_INT;
217
                    } elseif (is_int($param)) {
218 39
                        $type = PDO::PARAM_INT;
219
                    } elseif (is_null($param)) {
220 39
                        $type = PDO::PARAM_NULL;
221
                    } elseif ($param instanceof ModelInterface) {
222 39
                        $param = (int) $param->getId();
223
                        $type = PDO::PARAM_INT;
224
                    } else {
225 39
                        $type = PDO::PARAM_STR;
226
                    }
227
228 39
                    if (is_string($name)&&0) {
229
                        $query->bindValue($name, $param, $type);
230
                    } else {
231
                        $query->bindValue($i++, $param, $type);
232
                    }
233 39
                }
234 39
            }
235
236
            $result = $query->execute();
237
            if ($result === false) {
238 39
                $this->error("Unknown error");
239
            }
240 39
241 1
            $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...
242 1
243
            return $query;
244
        } catch (PDOException $e) {
245
            $this->error($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<PDOException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
246
        }
247
    }
248
249 1
    /**
250
     * Start a MySQL transaction
251 1
     */
252 1
    public function startTransaction()
253
    {
254
        $this->dbc->beginTransaction();
255
    }
256
257
    /**
258
     * Commit the stored queries (usable only if a transaction has been started)
259
     *
260
     * This does not show an error if there are no queries to commit
261
     */
262
    public function commit()
263
    {
264
        $this->dbc->commit();
265
    }
266
267
    /**
268
     * Cancel all pending queries (does not finish the transaction
269
     */
270
    public function rollback()
271
    {
272
        $this->dbc->rollBack();
273
    }
274
275 1
    /**
276
     * Commit all pending queries and finalise the transaction
277 1
     */
278 1
    public function finishTransaction()
279
    {
280
        $this->dbc->commit();
281
    }
282
283
    /**
284
     * Uses monolog to log an error message
285
     *
286
     * @param string         $error    The error string
287
     * @param int            $id       The error ID
288
     * @param Throwable|null $previous The exception that caused the error (if any)
289 1
     *
290
     * @throws Exception
291 1
     */
292
    public function error($error, $id = null, Throwable $previous = null)
293
    {
294
        if (empty($error)) {
295
            $error = "Unknown MySQL error - check for warnings generated by PHP";
296 1
        }
297 1
298 1
        // Create a context array so that we can log the ID, if provided
299
        $context = array();
300
        if ($id !== null) {
301 1
            $context['id'] = $id;
302 1
        }
303
304
        $this->logger->addError($error, $context);
305
        throw new Exception($error, (int) $id, $previous);
306
    }
307
308
    /**
309
     * Serialize the object
310
     *
311
     * Prevents PDO from being erroneously serialized
312
     *
313
     * @return array The list of properties that should be serialized
314
     */
315
    public function __sleep()
316
    {
317
        return array('last_id');
318
    }
319
}
320