Completed
Push — fm-support ( 85ae20...bc372a )
by Vladimir
05:37
created

Database::error()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 15
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 3
eloc 8
nc 4
nop 2
crap 3.0175
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
     * @var Database
21
     */
22
    private static $Database;
23
24
    /**
25
     * The database object used inside this class
26
     * @var MySQLi
27
     */
28
    private $dbc;
29
30
    /**
31
     * An instance of the logger
32
     * @var Logger
33
     */
34
    private $logger;
35
36
    /**
37
     * The id of the last row entered
38
     * @var int
39
     */
40
    private $last_id;
41
42
    /**
43
     * Create a new connection to the database
44
     *
45
     * @param string $host     The MySQL host
46
     * @param string $user     The MySQL user
47
     * @param string $password The MySQL password for the user
48
     * @param string $dbName   The MySQL database name
49
     *
50
     * @return Database A database object to interact with the database
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
51
     */
52 1
    public function __construct($host, $user, $password, $dbName)
53
    {
54 1
        if (Service::getContainer()) {
55 1
            if ($logger = Service::getContainer()->get('monolog.logger.mysql')) {
56 1
                $this->logger = $logger;
57
            }
58
        }
59
60 1
        $this->dbc = new mysqli($host, $user, $password, $dbName);
61
62 1
        if ($this->dbc->connect_errno) {
63
            $this->logger->addAlert($this->dbc->connect_error);
64
            throw new Exception($this->dbc->connect_error, $this->dbc->connect_errno);
65
        }
66
67 1
        $this->dbc->set_charset("utf8");
68 1
    }
69
70
    /**
71
     * Destroy this connection to the database
72
     */
73
    public function __destruct()
74
    {
75
        $this->closeConnection();
76
    }
77
78
    /**
79
     * Get an instance of the Database object
80
     *
81
     * This should be the main way to acquire access to the database
82
     *
83
     * @return Database The Database object
84
     */
85 39
    public static function getInstance()
86
    {
87 39
        if (!self::$Database) {
88 1
            if (Service::getEnvironment() == 'test') {
89 1
                if (!Service::getParameter('bzion.testing.enabled')) {
90
                    throw new Exception('You have to specify a MySQL database for testing in the bzion.testing section of your configuration file.');
91
                }
92
93 1
                self::$Database = new self(
94 1
                    Service::getParameter('bzion.testing.host'),
95 1
                    Service::getParameter('bzion.testing.username'),
96 1
                    Service::getParameter('bzion.testing.password'),
97 1
                    Service::getParameter('bzion.testing.database')
98
                );
99
            } else {
100
                self::$Database = new self(
101
                    Service::getParameter('bzion.mysql.host'),
102
                    Service::getParameter('bzion.mysql.username'),
103
                    Service::getParameter('bzion.mysql.password'),
104
                    Service::getParameter('bzion.mysql.database')
105
                );
106
            }
107
        }
108
109 39
        return self::$Database;
110
    }
111
112
    /**
113
     * Close the current connection to the MySQL database
114
     */
115
    public function closeConnection()
116
    {
117
        @mysqli_close($this->dbc);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
118
    }
119
120
    /**
121
     * Tests whether or not the connection to the database is still active
122
     * @return bool True if the connection is active
123
     */
124
    public function isConnected()
125
    {
126
        return $this->dbc->ping();
127
    }
128
129
    /**
130
     * Get the unique row ID of the last row that was inserted
131
     * @return int The ID of the row
132
     */
133 39
    public function getInsertId()
134
    {
135 39
        return $this->last_id;
136
    }
137
138
    /**
139
     * Prepares and executes a MySQL prepared statement. <em>Second two parameters are optional when using this function to execute a query with no placeholders.</em>
140
     *
141
     * <code>
142
     *      //the appropriate letters to show what type of variable will be passed
143
     *      //i - integer
144
     *      //d - double
145
     *      //s - string
146
     *      //b - blob
147
     *
148
     *      $database = new Database(); //create a new database object
149
     *
150
     *      $query = "SELECT * FROM table WHERE id = ?"; //write the prepared statement where ? are placeholders
151
     *      $params = array("1"); //all the parameters to be binded, in order
152
     *      $results = $database->query($query, "i", $params); //execute the prepared query
153
     * </code>
154
     *
155
     * @param  string      $queryText The prepared SQL statement that will be executed
156
     * @param  bool|string $typeDef   (Optional) The types of values that will be passed through the prepared statement. One letter per parameter
157
     * @param  mixed|array $params    (Optional) The array of values that will be binded to the prepared statement
158
     * @return mixed       Returns an array of the values received from the query or returns false on empty
159
     */
160 39
    public function query($queryText, $typeDef = false, $params = false)
161
    {
162 39
        if (!is_array($params)) {
163 39
            $params = array($params);
164
        }
165
166 39
        $debug = new DatabaseQuery($queryText, $typeDef, $params);
0 ignored issues
show
Bug introduced by
It seems like $typeDef defined by parameter $typeDef on line 160 can also be of type boolean; however, BZIon\Debug\DatabaseQuery::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
167
168 39
        $return = $this->doQuery($queryText, $typeDef, $params);
169
170 39
        $debug->finish($return);
171
172 39
        return $return;
173
    }
174
175
    /**
176
     * Perform a query
177
     * @param  string      $queryText The prepared SQL statement that will be executed
178
     * @param  bool|string $typeDef   (Optional) The types of values that will be passed through the prepared statement. One letter per parameter
179
     * @param  bool|array  $params    (Optional) The array of values that will be binded to the prepared statement
180
     * @return mixed       Returns an array of the values received from the query or returns false on empty
181
     */
182 39
    private function doQuery($queryText, $typeDef = false, $params = false)
183
    {
184 39
        $multiQuery = true;
185 39
        if ($stmt = $this->dbc->prepare($queryText)) {
186 39
            if (count($params) == count($params, 1)) {
187 39
                $params = array($params);
188 39
                $multiQuery = false;
189
            }
190
191 39
            if ($typeDef) {
192 39
                $bindParams = array();
193 39
                $bindParamsReferences = array();
194 39
                $bindParams = array_pad($bindParams, (count($params, 1) - count($params)) / count($params), "");
195
196 39
                foreach ($bindParams as $key => $value) {
197 39
                    $bindParamsReferences[$key] = &$bindParams[$key];
198
                }
199
200 39
                array_unshift($bindParamsReferences, $typeDef);
201 39
                $bindParamsMethod = new ReflectionMethod('mysqli_stmt', 'bind_param');
202 39
                $bindParamsMethod->invokeArgs($stmt, $bindParamsReferences);
203
            }
204
205 39
            $result = array();
206 39
            foreach ($params as $queryKey => $query) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
207 39
                if ($typeDef) {
208 39
                    foreach ($bindParams as $paramKey => $value) {
209 39
                        $bindParams[$paramKey] = $query[$paramKey];
0 ignored issues
show
Bug introduced by
The variable $bindParams does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
210
                    }
211
                }
212
213 39
                $queryResult = array();
214 39
                if ($stmt->execute()) {
215 39
                    $resultMetaData = $stmt->result_metadata();
216 39
                    $this->last_id = $stmt->insert_id;
217
218 39
                    if ($resultMetaData) {
219 39
                        $stmtRow = array();
220 39
                        $rowReferences = array();
221
222 39
                        while ($field = $resultMetaData->fetch_field()) {
223 39
                            $rowReferences[] = &$stmtRow[$field->name];
224
                        }
225
226 39
                        mysqli_free_result($resultMetaData);
227 39
                        $bindResultMethod = new ReflectionMethod('mysqli_stmt', 'bind_result');
228 39
                        $bindResultMethod->invokeArgs($stmt, $rowReferences);
229
230 39
                        while (mysqli_stmt_fetch($stmt)) {
231 39
                            $row = array();
232 39
                            foreach ($stmtRow as $key => $value) {
233 39
                                $row[$key] = $value;
234
                            }
235
236 39
                            $queryResult[] = $row;
237
                        }
238
239 39
                        mysqli_stmt_free_result($stmt);
240
                    } else {
241 39
                        $queryResult[] = mysqli_stmt_affected_rows($stmt);
242
                    }
243
                } else {
244 1
                    $this->error($this->dbc->error, $this->dbc->errno);
245
                    $queryResult[] = false;
246
                }
247
248 39
                $result[$queryKey] = $queryResult;
249
            }
250
251 39
            mysqli_stmt_close($stmt);
252
        } else {
253
            $result = false;
254
        }
255
256 39
        if ($this->dbc->error) {
257
            $this->error($this->dbc->error, $this->dbc->errno);
258
        }
259
260 39
        if ($multiQuery) {
261
            return $result;
262
        } else {
263 39
            return $result[0];
264
        }
265
    }
266
267
    /**
268
     * Start a MySQL transaction
269
     */
270 1
    public function startTransaction()
271
    {
272 1
        $this->dbc->autocommit(false);
273 1
    }
274
275
    /**
276
     * Commit the stored queries (usable only if a transaction has been started)
277
     *
278
     * This does not show an error if there are no queries to commit
279
     */
280
    public function commit()
281
    {
282
        $this->dbc->commit();
283
    }
284
285
    /**
286
     * Cancel all pending queries (does not finish the transaction
287
     */
288
    public function rollback()
289
    {
290
        $this->dbc->rollback();
291
    }
292
293
    /**
294
     * Commit all pending queries and finalise the transaction
295
     */
296 1
    public function finishTransaction()
297
    {
298 1
        $this->dbc->commit();
299 1
        $this->dbc->autocommit(true);
300 1
    }
301
302
    /**
303
     * Uses monolog to log an error message
304
     *
305
     * @param string $error The error string
306
     * @param int    $id    The error ID
307
     *
308
     * @throws Exception
309
     */
310 1
    public function error($error, $id = null)
311
    {
312 1
        if (empty($error)) {
313
            $error = "Unknown MySQL error - check for warnings generated by PHP";
314
        }
315
316
        // Create a context array so that we can log the ID, if provided
317 1
        $context = array();
318 1
        if ($id !== null) {
319 1
            $context['id'] = $id;
320
        }
321
322 1
        $this->logger->addError($error, $context);
323 1
        throw new Exception($error, $id);
324
    }
325
}
326