Subversion Repositories oidplus

Rev

Rev 1219 | Rev 1231 | 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 OIDplusDatabaseConnectionPDO extends OIDplusDatabaseConnection {
1130 daniel-mar 27
        /**
28
         * @var mixed|null
29
         */
635 daniel-mar 30
        private $conn = null;
1130 daniel-mar 31
 
32
        /**
33
         * @var string|null
34
         */
635 daniel-mar 35
        private $last_error = null; // we need that because PDO divides prepared statement errors and normal query errors, but we have only one "error()" method
1130 daniel-mar 36
 
37
        /**
38
         * @var bool
39
         */
635 daniel-mar 40
        private $transactions_supported = false;
41
 
1116 daniel-mar 42
        /**
43
         * @param string $sql
44
         * @param array|null $prepared_args
45
         * @return OIDplusQueryResultPDO
46
         * @throws OIDplusException
47
         */
48
        public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
635 daniel-mar 49
                $this->last_error = null;
50
                if (is_null($prepared_args)) {
51
                        $res = $this->conn->query($sql);
52
 
53
                        if ($res === false) {
54
                                $this->last_error = $this->conn->errorInfo()[2];
55
                                throw new OIDplusSQLException($sql, $this->error());
56
                        } else {
57
                                return new OIDplusQueryResultPDO($res);
58
                        }
59
                } else {
60
                        foreach ($prepared_args as &$value) {
61
                                // We need to manually convert booleans into strings, because there is a
62
                                // 14 year old bug that hasn't been adressed by the PDO developers:
63
                                // https://bugs.php.net/bug.php?id=57157
64
                                if (is_bool($value)) {
65
                                        if ($this->slangDetectionDone) {
66
                                                $value = $this->getSlang()->getSQLBool($value);
67
                                        } else {
68
                                                // This works for everything except Microsoft Access (which needs -1 and 0)
69
                                                // Note: We are using '1' and '0' instead of 'true' and 'false' because MySQL converts boolean to tinyint(1)
70
                                                $value = $value ? '1' : '0';
71
                                        }
72
                                }
73
                        }
74
 
75
                        $ps = $this->conn->prepare($sql);
76
                        if (!$ps) {
77
                                $this->last_error = $this->conn->errorInfo()[2];
1160 daniel-mar 78
                                if (!$this->last_error) $this->last_error = _L("Error")." ".$this->conn->errorInfo()[0]; // if no message is available, only show the error-code
635 daniel-mar 79
                                throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
80
                        }
81
 
1160 daniel-mar 82
                        if (!@$ps->execute($prepared_args)) {
635 daniel-mar 83
                                $this->last_error = $ps->errorInfo()[2];
1160 daniel-mar 84
                                if (!$this->last_error) $this->last_error = _L("Error")." ".$ps->errorInfo()[0]; // if no message is available, only show the error-code
85
                                // TODO:
86
                                // On my test machine with PDO + mysql on XAMPP with PHP 8.2.0, there are two problems with the following code:
87
                                //        $db->query("SELECT * from NONEXISTING", array(''));  // note that there is an additional argument, which is wrong!
88
                                //        $db->error()
89
                                // 1. $ps->errorInfo() is ['HY093', null, null].
90
                                //    The actual error message "Invalid parameter number: number of bound variables does not match number of tokens" is not shown via errorInfo()
91
                                //    => For now, as workaround, we just show the error message "HY093", if there is no driver specific error text available.
92
                                //       However, this means that the test-case will fail, because the table name cannot be found in the error message?!
93
                                // 2. The error "Invalid parameter number: number of bound variables does not match number of tokens" is SHOWN as PHP-warning
94
                                //    It seems like PDO::ERRMODE_SILENT is ignored?! The bug is 11 years old: https://bugs.php.net/bug.php?id=63812
95
                                //    => For now, as workaround, we added "@" in front of $ps->execute ...
96
                                //
97
                                // The following code works fine:
98
                                //        $db->query("SELECT * from NONEXISTING", array());  // note that there the number of arguments is now correct
99
                                //        $db->error()
100
                                // 1. $ps->errorInfo() is ['42S02', '1146', "Table 'oidplus.NONEXISTING' doesn't exist"].
101
                                //    => That's correct!
102
                                // 2. $ps->execute() does not show a warning (if "@" is removed)
103
                                //    => That's correct!
104
 
635 daniel-mar 105
                                throw new OIDplusSQLException($sql, $this->error());
106
                        }
107
                        return new OIDplusQueryResultPDO($ps);
108
                }
109
        }
110
 
1116 daniel-mar 111
        /**
112
         * @return int
113
         * @throws OIDplusException
114
         */
1160 daniel-mar 115
        public function doInsertId(): int {
635 daniel-mar 116
                try {
117
                        $out = @($this->conn->lastInsertId());
1167 daniel-mar 118
                        if ($out === false) return parent::doInsertId(); // fallback method that uses the SQL slang
635 daniel-mar 119
                        return $out;
1050 daniel-mar 120
                } catch (\Exception $e) {
1167 daniel-mar 121
                        return parent::doInsertId(); // fallback method that uses the SQL slang
635 daniel-mar 122
                }
123
        }
124
 
1116 daniel-mar 125
        /**
126
         * @return string
127
         */
635 daniel-mar 128
        public function error(): string {
129
                $err = $this->last_error;
130
                if ($err == null) $err = '';
1171 daniel-mar 131
                return vts_utf8_encode($err);
635 daniel-mar 132
        }
133
 
1116 daniel-mar 134
        /**
135
         * @return void
136
         * @throws OIDplusConfigInitializationException
137
         * @throws OIDplusException
138
         */
635 daniel-mar 139
        protected function doConnect()/*: void*/ {
140
                if (!class_exists('PDO')) throw new OIDplusConfigInitializationException(_L('PHP extension "%1" not installed','PDO'));
141
 
142
                try {
143
                        $options = [
1050 daniel-mar 144
                            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_SILENT,
145
                            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
146
                            \PDO::ATTR_EMULATE_PREPARES   => true,
635 daniel-mar 147
                        ];
148
 
149
                        // Try connecting to the database
1216 daniel-mar 150
                        $dsn      = OIDplus::baseConfig()->getValue('PDO_DSN',      'mysql:host=localhost;dbname=oidplus;charset=utf8mb4');
635 daniel-mar 151
                        $username = OIDplus::baseConfig()->getValue('PDO_USERNAME', 'root');
152
                        $password = OIDplus::baseConfig()->getValue('PDO_PASSWORD', '');
787 daniel-mar 153
 
1217 daniel-mar 154
                        if (stripos($dsn,"charset=") === false) {
1219 daniel-mar 155
                                // Try to extend DSN with charset
156
                                // Note: For MySQL, must be utf8 or utf8, and not UTF-8
1217 daniel-mar 157
                                try {
158
                                        $this->conn = new \PDO("$dsn;charset=utf8mb4", $username, $password, $options);
159
                                } catch (\Exception $e1) {
160
                                        try {
161
                                                $this->conn = new \PDO("$dsn;charset=utf8", $username, $password, $options);
162
                                        } catch (\Exception $e2) {
163
                                                try {
164
                                                        $this->conn = new \PDO("$dsn;charset=UTF-8", $username, $password, $options);
165
                                                } catch (\Exception $e3) {
166
                                                        $this->conn = new \PDO($dsn, $username, $password, $options);
167
                                                }
168
                                        }
169
                                }
1218 daniel-mar 170
                        } else {
171
                                $this->conn = new \PDO($dsn, $username, $password, $options);
1217 daniel-mar 172
                        }
1050 daniel-mar 173
                } catch (\PDOException $e) {
635 daniel-mar 174
                        $message = $e->getMessage();
1171 daniel-mar 175
                        $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
863 daniel-mar 176
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 177
                }
178
 
179
                $this->last_error = null;
180
 
181
                try {
1216 daniel-mar 182
                        @$this->conn->exec("SET NAMES 'utf-8'");
1050 daniel-mar 183
                } catch (\Exception $e) {
635 daniel-mar 184
                }
185
 
1216 daniel-mar 186
                try {
187
                        @$this->conn->exec("SET CHARACTER SET 'utf-8'");
188
                } catch (\Exception $e) {
189
                }
190
 
191
                try {
192
                        @$this->conn->exec("SET NAMES 'utf8mb4'");
193
                } catch (\Exception $e) {
194
                }
195
 
635 daniel-mar 196
                // We check if the DBMS supports autocommit.
197
                // Attention: Check it after you have sent a query already, because Microsoft Access doesn't seem to allow
198
                // changing auto commit once a query was executed ("Attribute cannot be set now SQLState: S1011")
199
                // Note: For some weird reason we *DO* need to redirect the output to "$dummy", otherwise it won't work!
200
                $sql = "select name from ###config where 1=0";
201
                $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
202
                $dummy = $this->conn->query($sql);
203
                try {
204
                        $this->conn->beginTransaction();
205
                        $this->conn->rollBack();
206
                        $this->transactions_supported = true;
1050 daniel-mar 207
                } catch (\Exception $e) {
635 daniel-mar 208
                        $this->transactions_supported = false;
209
                }
210
        }
211
 
1116 daniel-mar 212
        /**
213
         * @return void
214
         */
635 daniel-mar 215
        protected function doDisconnect()/*: void*/ {
216
                $this->conn = null; // the connection will be closed by removing the reference
217
        }
218
 
1116 daniel-mar 219
        /**
220
         * @var bool
221
         */
635 daniel-mar 222
        private $intransaction = false;
223
 
1116 daniel-mar 224
        /**
225
         * @return bool
226
         */
635 daniel-mar 227
        public function transaction_supported(): bool {
228
                return $this->transactions_supported;
229
        }
230
 
1116 daniel-mar 231
        /**
232
         * @return int
233
         */
635 daniel-mar 234
        public function transaction_level(): int {
235
                if (!$this->transaction_supported()) {
236
                        // TODO?
237
                        return 0;
238
                }
239
                return $this->intransaction ? 1 : 0;
240
        }
241
 
1116 daniel-mar 242
        /**
243
         * @return void
244
         * @throws OIDplusException
245
         */
635 daniel-mar 246
        public function transaction_begin()/*: void*/ {
247
                if (!$this->transaction_supported()) {
248
                        // TODO?
249
                        return;
250
                }
251
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
252
                $this->conn->beginTransaction();
253
                $this->intransaction = true;
254
        }
255
 
1116 daniel-mar 256
        /**
257
         * @return void
258
         */
635 daniel-mar 259
        public function transaction_commit()/*: void*/ {
260
                if (!$this->transaction_supported()) {
261
                        // TODO?
262
                        return;
263
                }
264
                $this->conn->commit();
265
                $this->intransaction = false;
266
        }
267
 
1116 daniel-mar 268
        /**
269
         * @return void
270
         */
635 daniel-mar 271
        public function transaction_rollback()/*: void*/ {
272
                if (!$this->transaction_supported()) {
273
                        // TODO?
274
                        return;
275
                }
276
                $this->conn->rollBack();
277
                $this->intransaction = false;
278
        }
279
 
1220 daniel-mar 280
        /**
281
         * @return array
282
         */
283
        public function getExtendedInfo(): array {
284
                $dsn = OIDplus::baseConfig()->getValue('PDO_DSN', 'mysql:host=localhost;dbname=oidplus;charset=utf8mb4');
285
                return array(
286
                        _L('DSN') => $dsn
287
                );
288
        }
289
 
635 daniel-mar 290
}