Subversion Repositories oidplus

Rev

Rev 1316 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
635 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
635 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
635 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
635 daniel-mar 26
class OIDplusDatabaseConnectionMySQLi extends OIDplusDatabaseConnection {
1130 daniel-mar 27
 
28
        /**
29
         * @var mixed|null
30
         */
635 daniel-mar 31
        private $conn = null; // only with MySQLnd
1130 daniel-mar 32
 
33
        /**
34
         * @var array
35
         */
635 daniel-mar 36
        private $prepare_cache = array();
1130 daniel-mar 37
 
38
        /**
39
         * @var string|null
40
         */
635 daniel-mar 41
        private $last_error = null; // we need that because MySQL divides prepared statement errors and normal query errors, but we have only one "error()" method
42
 
1116 daniel-mar 43
        /**
44
         * @param string $sql
45
         * @param array|null $prepared_args
46
         * @return OIDplusQueryResultMySQL|OIDplusQueryResultMySQLNoNativeDriver
47
         * @throws OIDplusException
48
         */
49
        public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
635 daniel-mar 50
                $this->last_error = null;
51
                if (is_null($prepared_args)) {
1155 daniel-mar 52
                        try {
53
                                $res = $this->conn->query($sql, MYSQLI_STORE_RESULT);
54
                        } catch (\Exception $e) {
55
                                $this->last_error = $e->getMessage();
56
                                throw new OIDplusSQLException($sql, $e->getMessage());
57
                        }
635 daniel-mar 58
 
59
                        if ($res === false) {
60
                                $this->last_error = $this->conn->error;
61
                                throw new OIDplusSQLException($sql, $this->error());
62
                        } else {
63
                                return new OIDplusQueryResultMySQL($res);
64
                        }
65
                } else {
66
                        foreach ($prepared_args as &$value) {
67
                                // MySQLi has problems converting "true/false" to the data type "tinyint(1)"
1116 daniel-mar 68
                                // It seems to be the same issue as in PDO reported 14 years ago at https://bugs.php.net/bug.php?id=57157
635 daniel-mar 69
                                if (is_bool($value)) $value = $value ? '1' : '0';
70
                        }
1316 daniel-mar 71
                        unset($value);
635 daniel-mar 72
 
73
                        if (isset($this->prepare_cache[$sql])) {
74
                                $ps = $this->prepare_cache[$sql];
75
                        } else {
1155 daniel-mar 76
                                try {
77
                                        $ps = $this->conn->prepare($sql);
78
                                } catch (\Exception $e) {
79
                                        $this->last_error = $e->getMessage();
80
                                        throw new OIDplusSQLException($sql, $e->getMessage());
81
                                }
635 daniel-mar 82
                                if (!$ps) {
83
                                        $this->last_error = $this->conn->error;
84
                                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
85
                                }
86
 
87
                                // Caching the prepared is very risky
88
                                // In PDO and ODBC we may not do it, because execute() will
89
                                // destroy the existing cursors.
90
                                // (test this with ./?goto=oid%3A1.3.6.1.4.1.37553.8.32488192274
91
                                // you will see that 2.999 is missing in the tree)
92
                                // But $ps->get_result() seems to "clone" the cursor,
93
                                // so that $ps->execute may be called a second time?!
94
                                // However, it only works with mysqlnd's get_result,
95
                                // not with OIDplusQueryResultMySQLNoNativeDriver
96
                                if (self::nativeDriverAvailable()) {
97
                                        $this->prepare_cache[$sql] = $ps;
98
                                }
99
                        }
100
 
101
                        self::bind_placeholder_vars($ps,$prepared_args);
102
                        if (!$ps->execute()) {
103
                                $this->last_error = mysqli_stmt_error($ps);
104
                                throw new OIDplusSQLException($sql, $this->error());
105
                        }
106
 
107
                        if (self::nativeDriverAvailable()) {
108
                                return new OIDplusQueryResultMySQL($ps->get_result());
109
                        } else {
110
                                return new OIDplusQueryResultMySQLNoNativeDriver($ps);
111
                        }
112
                }
113
        }
114
 
1116 daniel-mar 115
        /**
116
         * @return int
117
         */
1160 daniel-mar 118
        public function doInsertId(): int {
635 daniel-mar 119
                return $this->conn->insert_id;
120
        }
121
 
1116 daniel-mar 122
        /**
123
         * @return string
124
         */
635 daniel-mar 125
        public function error(): string {
126
                $err = $this->last_error;
1155 daniel-mar 127
                if ($err === null) $err = '';
635 daniel-mar 128
                return $err;
129
        }
130
 
1116 daniel-mar 131
        /**
132
         * @return void
133
         * @throws OIDplusConfigInitializationException
134
         * @throws OIDplusException
135
         */
635 daniel-mar 136
        protected function doConnect()/*: void*/ {
137
                if (!function_exists('mysqli_connect')) throw new OIDplusException(_L('PHP extension "%1" not installed','MySQLi'));
138
 
139
                // Try connecting to the database
140
                $host     = OIDplus::baseConfig()->getValue('MYSQL_HOST',     'localhost');
141
                $username = OIDplus::baseConfig()->getValue('MYSQL_USERNAME', 'root');
142
                $password = OIDplus::baseConfig()->getValue('MYSQL_PASSWORD', '');
143
                $database = OIDplus::baseConfig()->getValue('MYSQL_DATABASE', 'oidplus');
813 daniel-mar 144
                $socket   = OIDplus::baseConfig()->getValue('MYSQL_SOCKET',   '');
635 daniel-mar 145
                list($hostname,$port) = explode(':', $host.':'.ini_get("mysqli.default_port"));
146
                $port = intval($port);
1050 daniel-mar 147
                $this->conn = @new \mysqli($hostname, $username, $password, $database, $port, $socket);
635 daniel-mar 148
                if (!empty($this->conn->connect_error) || ($this->conn->connect_errno != 0)) {
149
                        $message = $this->conn->connect_error;
863 daniel-mar 150
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 151
                }
152
 
153
                $this->prepare_cache = array();
154
                $this->last_error = null;
155
 
1214 daniel-mar 156
                $this->query("SET NAMES 'utf8mb4'");
635 daniel-mar 157
        }
158
 
1116 daniel-mar 159
        /**
160
         * @return void
161
         */
635 daniel-mar 162
        protected function doDisconnect()/*: void*/ {
163
                $this->prepare_cache = array();
164
                if (!is_null($this->conn)) {
165
                        $this->conn->close();
166
                        $this->conn = null;
167
                }
168
        }
169
 
1116 daniel-mar 170
        /**
171
         * @var bool
172
         */
635 daniel-mar 173
        private $intransaction = false;
174
 
1116 daniel-mar 175
        /**
176
         * @return bool
177
         */
635 daniel-mar 178
        public function transaction_supported(): bool {
179
                return true;
180
        }
181
 
1116 daniel-mar 182
        /**
183
         * @return int
184
         */
635 daniel-mar 185
        public function transaction_level(): int {
186
                return $this->intransaction ? 1 : 0;
187
        }
188
 
1116 daniel-mar 189
        /**
190
         * @return void
191
         * @throws OIDplusException
192
         */
635 daniel-mar 193
        public function transaction_begin()/*: void*/ {
194
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
195
                $this->conn->autocommit(false);
196
                $this->conn->begin_transaction();
197
                $this->intransaction = true;
198
        }
199
 
1116 daniel-mar 200
        /**
201
         * @return void
202
         */
635 daniel-mar 203
        public function transaction_commit()/*: void*/ {
204
                $this->conn->commit();
205
                $this->conn->autocommit(true);
206
                $this->intransaction = false;
207
        }
208
 
1116 daniel-mar 209
        /**
210
         * @return void
211
         */
635 daniel-mar 212
        public function transaction_rollback()/*: void*/ {
213
                $this->conn->rollback();
214
                $this->conn->autocommit(true);
215
                $this->intransaction = false;
216
        }
217
 
1116 daniel-mar 218
        /**
219
         * @return string
220
         */
635 daniel-mar 221
        public function sqlDate(): string {
222
                return 'now()';
223
        }
224
 
1116 daniel-mar 225
        /**
226
         * @return bool
227
         * @throws OIDplusException
228
         */
635 daniel-mar 229
        public static function nativeDriverAvailable(): bool {
230
                return function_exists('mysqli_fetch_all') && (OIDplus::baseConfig()->getValue('MYSQL_FORCE_MYSQLND_SUPPLEMENT', false) === false);
231
        }
232
 
1116 daniel-mar 233
        /**
234
         * @param object $stmt
235
         * @param array $params
236
         * @return bool
237
         */
1128 daniel-mar 238
        private static function bind_placeholder_vars(&$stmt, array $params): bool {
239
                // Note: "object" is not a type-hint!
635 daniel-mar 240
                // Credit to: Dave Morgan
241
                // Code taken from: http://www.devmorgan.com/blog/2009/03/27/dydl-part-3-dynamic-binding-with-mysqli-php/
242
                //                  https://stackoverflow.com/questions/17219214/how-to-bind-in-mysqli-dynamically
243
                if ($params != null) {
244
                        $types = '';                        //initial sting with types
245
                        foreach ($params as $param) {        //for each element, determine type and add
246
                                if (is_int($param)) {
247
                                        $types .= 'i';              //integer
248
                                } elseif (is_float($param)) {
249
                                        $types .= 'd';              //double
250
                                } elseif (is_string($param)) {
251
                                        $types .= 's';              //string
252
                                } else {
253
                                        $types .= 'b';              //blob and unknown
254
                                }
255
                        }
256
 
257
                        $bind_names = array();
258
                        $bind_names[] = $types;             //first param needed is the type string, e.g.: 'issss'
259
 
260
                        for ($i=0; $i<count($params);$i++) {    //go through incoming params and added em to array
261
                                $bind_name = 'bind' . $i;       //give them an arbitrary name
262
                                $$bind_name = $params[$i];      //add the parameter to the variable variable
263
                                $bind_names[] = &$$bind_name;   //now associate the variable as an element in an array
264
                        }
265
 
266
                        //error_log("better_mysqli has params ".print_r($bind_names, 1));
267
                        //call the function bind_param with dynamic params
268
                        call_user_func_array(array($stmt,'bind_param'),$bind_names);
269
                        return true;
270
                } else {
271
                        return false;
272
                }
273
        }
274
 
1116 daniel-mar 275
        /**
276
         * @param bool $mustExist
277
         * @return OIDplusSqlSlangPlugin|null
278
         * @throws OIDplusConfigInitializationException
279
         */
635 daniel-mar 280
        protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
281
                $slang = OIDplus::getSqlSlangPlugin('mysql');
282
                if (is_null($slang)) {
1430 daniel-mar 283
                        throw new OIDplusConfigInitializationException(_L('SQL-Slang plugin "%1" is missing. Please check if it exists in the directory "plugin/sqlSlang". If it is not existing, please recover it from an GIT/SVN snapshot or OIDplus archive file.','mysql'));
635 daniel-mar 284
                }
285
                return $slang;
286
        }
1220 daniel-mar 287
 
288
        /**
289
         * @return array
290
         */
291
        public function getExtendedInfo(): array {
292
                $host     = OIDplus::baseConfig()->getValue('MYSQL_HOST',     'localhost');
293
                $username = OIDplus::baseConfig()->getValue('MYSQL_USERNAME', 'root');
294
                $password = OIDplus::baseConfig()->getValue('MYSQL_PASSWORD', '');
295
                $database = OIDplus::baseConfig()->getValue('MYSQL_DATABASE', 'oidplus');
296
                $socket   = OIDplus::baseConfig()->getValue('MYSQL_SOCKET',   '');
297
                list($hostname,$port) = explode(':', $host.':'.ini_get("mysqli.default_port"));
298
                $port = intval($port);
299
                return array(
300
                        _L('Hostname') => $hostname,
301
                        _L('Port') => $port,
302
                        _L('Socket') => $socket,
303
                        _L('Database') => $database,
304
                        _L('Username') => $username
305
                );
306
        }
635 daniel-mar 307
}