Subversion Repositories oidplus

Rev

Rev 1128 | Rev 1155 | Go to most recent revision | 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)) {
52
                        $res = $this->conn->query($sql, MYSQLI_STORE_RESULT);
53
 
54
                        if ($res === false) {
55
                                $this->last_error = $this->conn->error;
56
                                throw new OIDplusSQLException($sql, $this->error());
57
                        } else {
58
                                return new OIDplusQueryResultMySQL($res);
59
                        }
60
                } else {
61
                        if (!is_array($prepared_args)) {
62
                                throw new OIDplusException(_L('"prepared_args" must be either NULL or an ARRAY.'));
63
                        }
64
 
65
                        foreach ($prepared_args as &$value) {
66
                                // MySQLi has problems converting "true/false" to the data type "tinyint(1)"
1116 daniel-mar 67
                                // 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 68
                                if (is_bool($value)) $value = $value ? '1' : '0';
69
                        }
70
 
71
                        if (isset($this->prepare_cache[$sql])) {
72
                                $ps = $this->prepare_cache[$sql];
73
                        } else {
74
                                $ps = $this->conn->prepare($sql);
75
                                if (!$ps) {
76
                                        $this->last_error = $this->conn->error;
77
                                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
78
                                }
79
 
80
                                // Caching the prepared is very risky
81
                                // In PDO and ODBC we may not do it, because execute() will
82
                                // destroy the existing cursors.
83
                                // (test this with ./?goto=oid%3A1.3.6.1.4.1.37553.8.32488192274
84
                                // you will see that 2.999 is missing in the tree)
85
                                // But $ps->get_result() seems to "clone" the cursor,
86
                                // so that $ps->execute may be called a second time?!
87
                                // However, it only works with mysqlnd's get_result,
88
                                // not with OIDplusQueryResultMySQLNoNativeDriver
89
                                if (self::nativeDriverAvailable()) {
90
                                        $this->prepare_cache[$sql] = $ps;
91
                                }
92
                        }
93
 
94
                        self::bind_placeholder_vars($ps,$prepared_args);
95
                        if (!$ps->execute()) {
96
                                $this->last_error = mysqli_stmt_error($ps);
97
                                throw new OIDplusSQLException($sql, $this->error());
98
                        }
99
 
100
                        if (self::nativeDriverAvailable()) {
101
                                return new OIDplusQueryResultMySQL($ps->get_result());
102
                        } else {
103
                                return new OIDplusQueryResultMySQLNoNativeDriver($ps);
104
                        }
105
                }
106
        }
107
 
1116 daniel-mar 108
        /**
109
         * @return int
110
         */
635 daniel-mar 111
        public function insert_id(): int {
112
                return $this->conn->insert_id;
113
        }
114
 
1116 daniel-mar 115
        /**
116
         * @return string
117
         */
635 daniel-mar 118
        public function error(): string {
119
                $err = $this->last_error;
120
                if ($err == null) $err = '';
121
                return $err;
122
        }
123
 
1116 daniel-mar 124
        /**
125
         * @return void
126
         * @throws OIDplusConfigInitializationException
127
         * @throws OIDplusException
128
         */
635 daniel-mar 129
        protected function doConnect()/*: void*/ {
130
                if (!function_exists('mysqli_connect')) throw new OIDplusException(_L('PHP extension "%1" not installed','MySQLi'));
131
 
132
                // Try connecting to the database
133
                $host     = OIDplus::baseConfig()->getValue('MYSQL_HOST',     'localhost');
134
                $username = OIDplus::baseConfig()->getValue('MYSQL_USERNAME', 'root');
135
                $password = OIDplus::baseConfig()->getValue('MYSQL_PASSWORD', '');
136
                $database = OIDplus::baseConfig()->getValue('MYSQL_DATABASE', 'oidplus');
813 daniel-mar 137
                $socket   = OIDplus::baseConfig()->getValue('MYSQL_SOCKET',   '');
635 daniel-mar 138
                list($hostname,$port) = explode(':', $host.':'.ini_get("mysqli.default_port"));
139
                $port = intval($port);
1050 daniel-mar 140
                $this->conn = @new \mysqli($hostname, $username, $password, $database, $port, $socket);
635 daniel-mar 141
                if (!empty($this->conn->connect_error) || ($this->conn->connect_errno != 0)) {
142
                        $message = $this->conn->connect_error;
863 daniel-mar 143
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 144
                }
145
 
146
                $this->prepare_cache = array();
147
                $this->last_error = null;
148
 
149
                $this->query("SET NAMES 'utf8'");
150
        }
151
 
1116 daniel-mar 152
        /**
153
         * @return void
154
         */
635 daniel-mar 155
        protected function doDisconnect()/*: void*/ {
156
                $this->prepare_cache = array();
157
                if (!is_null($this->conn)) {
158
                        $this->conn->close();
159
                        $this->conn = null;
160
                }
161
        }
162
 
1116 daniel-mar 163
        /**
164
         * @var bool
165
         */
635 daniel-mar 166
        private $intransaction = false;
167
 
1116 daniel-mar 168
        /**
169
         * @return bool
170
         */
635 daniel-mar 171
        public function transaction_supported(): bool {
172
                return true;
173
        }
174
 
1116 daniel-mar 175
        /**
176
         * @return int
177
         */
635 daniel-mar 178
        public function transaction_level(): int {
179
                return $this->intransaction ? 1 : 0;
180
        }
181
 
1116 daniel-mar 182
        /**
183
         * @return void
184
         * @throws OIDplusException
185
         */
635 daniel-mar 186
        public function transaction_begin()/*: void*/ {
187
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
188
                $this->conn->autocommit(false);
189
                $this->conn->begin_transaction();
190
                $this->intransaction = true;
191
        }
192
 
1116 daniel-mar 193
        /**
194
         * @return void
195
         */
635 daniel-mar 196
        public function transaction_commit()/*: void*/ {
197
                $this->conn->commit();
198
                $this->conn->autocommit(true);
199
                $this->intransaction = false;
200
        }
201
 
1116 daniel-mar 202
        /**
203
         * @return void
204
         */
635 daniel-mar 205
        public function transaction_rollback()/*: void*/ {
206
                $this->conn->rollback();
207
                $this->conn->autocommit(true);
208
                $this->intransaction = false;
209
        }
210
 
1116 daniel-mar 211
        /**
212
         * @return string
213
         */
635 daniel-mar 214
        public function sqlDate(): string {
215
                return 'now()';
216
        }
217
 
1116 daniel-mar 218
        /**
219
         * @return bool
220
         * @throws OIDplusException
221
         */
635 daniel-mar 222
        public static function nativeDriverAvailable(): bool {
223
                return function_exists('mysqli_fetch_all') && (OIDplus::baseConfig()->getValue('MYSQL_FORCE_MYSQLND_SUPPLEMENT', false) === false);
224
        }
225
 
1116 daniel-mar 226
        /**
227
         * @param object $stmt
228
         * @param array $params
229
         * @return bool
230
         */
1128 daniel-mar 231
        private static function bind_placeholder_vars(&$stmt, array $params): bool {
232
                // Note: "object" is not a type-hint!
635 daniel-mar 233
                // Credit to: Dave Morgan
234
                // Code taken from: http://www.devmorgan.com/blog/2009/03/27/dydl-part-3-dynamic-binding-with-mysqli-php/
235
                //                  https://stackoverflow.com/questions/17219214/how-to-bind-in-mysqli-dynamically
236
                if ($params != null) {
237
                        $types = '';                        //initial sting with types
238
                        foreach ($params as $param) {        //for each element, determine type and add
239
                                if (is_int($param)) {
240
                                        $types .= 'i';              //integer
241
                                } elseif (is_float($param)) {
242
                                        $types .= 'd';              //double
243
                                } elseif (is_string($param)) {
244
                                        $types .= 's';              //string
245
                                } else {
246
                                        $types .= 'b';              //blob and unknown
247
                                }
248
                        }
249
 
250
                        $bind_names = array();
251
                        $bind_names[] = $types;             //first param needed is the type string, e.g.: 'issss'
252
 
253
                        for ($i=0; $i<count($params);$i++) {    //go through incoming params and added em to array
254
                                $bind_name = 'bind' . $i;       //give them an arbitrary name
255
                                $$bind_name = $params[$i];      //add the parameter to the variable variable
256
                                $bind_names[] = &$$bind_name;   //now associate the variable as an element in an array
257
                        }
258
 
259
                        //error_log("better_mysqli has params ".print_r($bind_names, 1));
260
                        //call the function bind_param with dynamic params
261
                        call_user_func_array(array($stmt,'bind_param'),$bind_names);
262
                        return true;
263
                } else {
264
                        return false;
265
                }
266
        }
267
 
1116 daniel-mar 268
        /**
269
         * @param bool $mustExist
270
         * @return OIDplusSqlSlangPlugin|null
271
         * @throws OIDplusConfigInitializationException
272
         */
635 daniel-mar 273
        protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
274
                $slang = OIDplus::getSqlSlangPlugin('mysql');
275
                if (is_null($slang)) {
276
                        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 SVN snapshot or OIDplus TAR.GZ file.','mysql'));
277
                }
278
                return $slang;
279
        }
280
}