Passed
Push — master ( 6b9ef6...4302eb )
by Stefan
10:02
created

DBConnection::exec()   D

Complexity

Conditions 18
Paths 117

Size

Total Lines 75
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 75
rs 4.725
c 0
b 0
f 0
cc 18
nc 117
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the DBConnection singleton.
25
 * 
26
 * @author Stefan Winter <[email protected]>
27
 * @author Tomasz Wolniewicz <[email protected]>
28
 * 
29
 * @package Developer
30
 */
31
32
namespace core;
33
34
use \Exception;
35
36
/**
37
 * This class is a singleton for establishing a connection to the database
38
 *
39
 * @author Stefan Winter <[email protected]>
40
 * @author Tomasz Wolniewicz <[email protected]>
41
 *
42
 * @license see LICENSE file in root directory
43
 *
44
 * @package Developer
45
 */
46
class DBConnection {
47
48
    /**
49
     * This is the actual constructor for the singleton. It creates a database connection if it is not up yet, and returns a handle to the database connection on every call.
50
     * 
51
     * @param string $database the database type to open
52
     * @return DBConnection the (only) instance of this class
53
     * @throws Exception
54
     */
55
    public static function handle($database) {
56
        $theDb = strtoupper($database);
57
        switch ($theDb) {
58
            case "INST":
59
            case "USER":
60
            case "EXTERNAL":
61
            case "FRONTEND":
62
            case "DIAGNOSTICS":
63
                if (!isset(self::${"instance" . $theDb})) {
64
                    $class = __CLASS__;
65
                    self::${"instance" . $theDb} = new $class($database);
66
                    DBConnection::${"instance" . $theDb}->databaseInstance = $theDb;
67
                }
68
                return self::${"instance" . $theDb};
69
            default:
70
                throw new Exception("This type of database (" . strtoupper($database) . ") is not known!");
71
        }
72
    }
73
74
    /**
75
     * Implemented for safety reasons only. Cloning is forbidden and will tell the user so.
76
     *
77
     * @return void
78
     */
79
    public function __clone() {
80
        trigger_error('Clone is not allowed.', E_USER_ERROR);
81
    }
82
83
    /**
84
     * tells the caller if the database is to be accessed read-only
85
     * @return bool
86
     */
87
    public function isReadOnly() {
88
        return $this->readOnly;
89
    }
90
91
    /**
92
     * executes a query and triggers logging to the SQL audit log if it's not a SELECT
93
     * @param string $querystring  the query to be executed
94
     * @param string $types        for prepared statements, the type list of parameters
95
     * @param mixed  ...$arguments for prepared statements, the parameters
96
     * @return mixed the query result as mysqli_result object; or TRUE on non-return-value statements
97
     * @throws Exception
98
     */
99
    public function exec($querystring, $types = NULL, &...$arguments) {
100
        // log exact query to audit log, if it's not a SELECT
101
        $isMoreThanSelect = FALSE;
102
        if (preg_match("/^(SELECT\ |SET\ )/i", $querystring) == 0 && preg_match("/^DESC/i", $querystring) == 0) {
103
            $isMoreThanSelect = TRUE;
104
            if ($this->readOnly) { // let's not do this.
105
                throw new Exception("This is a read-only DB connection, but this is statement is not a SELECT!");
106
            }
107
        }
108
        // log exact query to debug log, if log level is at 5
109
        $this->loggerInstance->debug(5, "DB ATTEMPT: " . $querystring . "\n");
110
        if ($types !== NULL) {
111
            $this->loggerInstance->debug(5, "Argument type sequence: $types, parameters are: " . print_r($arguments, true));
112
        }
113
114
        if ($this->connection->connect_error) {
115
            throw new Exception("ERROR: Cannot send query to $this->databaseInstance database (no connection, error number" . $this->connection->connect_error . ")!");
116
        }
117
        if ($types === NULL) {
118
            $result = $this->connection->query($querystring);
119
            if ($result === FALSE) {
120
                throw new Exception("DB: Unable to execute simple statement! Error was --> " . $this->connection->error . " <--");
121
            }
122
        } else {
123
            // fancy! prepared statement with dedicated argument list
124
            if (strlen($types) != count($arguments)) {
125
                throw new Exception("DB: Prepared Statement: Number of arguments and the type list length differ!");
126
            }
127
            if (isset($this->preparedStatements[$querystring])) {
128
                $statementObject = $this->preparedStatements[$querystring];
129
            } else {
130
                $statementObject = $this->connection->stmt_init();
131
                if ($statementObject === FALSE) {
132
                    throw new Exception("DB: Unable to initialise prepared Statement!");
133
                }
134
                $prepResult = $statementObject->prepare($querystring);
135
                if ($prepResult === FALSE) {
136
                    throw new Exception("DB: Unable to prepare statement! Statement was --> $querystring <--, error was --> " . $statementObject->error . " <--.");
137
                }
138
                $this->preparedStatements[$querystring] = $statementObject;
139
            }
140
            // we have a variable number of arguments packed into the ... array
141
            // but the function needs to be called exactly once, with a series of
142
            // individual arguments, not an array. The voodoo solution is to call
143
            // it via call_user_func_array()
144
145
            $localArray = $arguments;
146
            array_unshift($localArray, $types);
147
            $retval = call_user_func_array([$statementObject, "bind_param"], $localArray);
148
            if ($retval === FALSE) {
149
                throw new Exception("DB: Unable to bind parameters to prepared statement! Argument array was --> " . var_export($localArray, TRUE) . " <--. Error was --> " . $statementObject->error . " <--");
150
            }
151
            $result = $statementObject->execute();
152
            if ($result === FALSE) {
153
                throw new Exception("DB: Unable to execute prepared statement! Error was --> " . $statementObject->error . " <--");
154
            }
155
            $selectResult = $statementObject->get_result();
156
            if ($selectResult !== FALSE) {
157
                $result = $selectResult;
158
            }
159
        }
160
161
        // all cases where $result could be FALSE have been caught earlier
162
        if ($this->connection->errno) {
163
            throw new Exception("ERROR: Cannot execute query in $this->databaseInstance database - (hopefully escaped) query was '$querystring', errno was " . $this->connection->errno . "!");
164
        }
165
166
167
        if ($isMoreThanSelect) {
168
            $this->loggerInstance->writeSQLAudit("[DB: " . strtoupper($this->databaseInstance) . "] " . $querystring);
169
            if ($types !== NULL) {
170
                $this->loggerInstance->writeSQLAudit("Argument type sequence: $types, parameters are: " . print_r($arguments, true));
171
            }
172
        }
173
        return $result;
174
    }
175
176
    /**
177
     * Retrieves the last auto-id of an INSERT. Needs to be called immediately after the corresponding exec() call
178
     * @return int the last autoincrement-ID
179
     */
180
    public function lastID() {
181
        return $this->connection->insert_id;
182
    }
183
184
    /**
185
     * Holds the singleton instance reference to USER database
186
     * 
187
     * @var DBConnection 
188
     */
189
    private static $instanceUSER;
190
191
    /**
192
     * Holds the singleton instance reference to INST database
193
     * 
194
     * @var DBConnection 
195
     */
196
    private static $instanceINST;
197
198
    /**
199
     * Holds the singleton instance reference to EXTERNAL database
200
     * 
201
     * @var DBConnection 
202
     */
203
    private static $instanceEXTERNAL;
204
205
    /**
206
     * Holds the singleton instance reference to FRONTEND database
207
     * 
208
     * @var DBConnection 
209
     */
210
    private static $instanceFRONTEND;
211
212
    /**
213
     * Holds the singleton instance reference to DIAGNOSTICS database
214
     * 
215
     * @var DBConnection 
216
     */
217
    private static $instanceDIAGNOSTICS;
218
219
    /**
220
     * after instantiation, keep state of which DB *this one* talks to
221
     * 
222
     * @var string which database does this instance talk to
223
     */
224
    private $databaseInstance;
225
226
    /**
227
     * The connection to the DB server
228
     * 
229
     * @var \mysqli
230
     */
231
    private $connection;
232
233
    /**
234
     * @var \core\common\Logging
235
     */
236
    private $loggerInstance;
237
238
    /**
239
     * Keeps state whether we are a readonly DB instance
240
     * @var boolean
241
     */
242
    private $readOnly;
243
244
    /**
245
     * Class constructor. Cannot be called directly; use handle()
246
     * 
247
     * @param string $database the database to open
248
     * @throws Exception
249
     */
250
    private function __construct($database) {
251
        $this->loggerInstance = new \core\common\Logging();
252
        $databaseCapitalised = strtoupper($database);
253
        $this->connection = new \mysqli(\config\Master::DB[$databaseCapitalised]['host'], \config\Master::DB[$databaseCapitalised]['user'], \config\Master::DB[$databaseCapitalised]['pass'], \config\Master::DB[$databaseCapitalised]['db']);
254
        if ($this->connection->connect_error) {
255
            throw new Exception("ERROR: Unable to connect to $database database! This is a fatal error, giving up (error number " . $this->connection->connect_errno . ").");
256
        }
257
        $this->readOnly = \config\Master::DB[$databaseCapitalised]['readonly'];
258
    }
259
260
    /**
261
     * keeps all previously prepared statements in memory so we can reuse them
262
     * later
263
     * 
264
     * @var array
265
     */
266
    private $preparedStatements = [];
267
268
}
269