Subversion Repositories oidplus

Rev

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