├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon └── src └── voku └── db ├── DB.php ├── Debug.php ├── Helper.php ├── Prepare.php ├── Result.php └── exceptions ├── DBConnectException.php ├── DBGoneAwayException.php ├── FetchingException.php └── QueryException.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 8.3.1 (2022-07-29) 5 | 6 | - fix sql syntax of "BETWEEN" queries 7 | - update "symfony/property-access" 8 | 9 | 8.3.0 (2022-01-13) 10 | 11 | - remove get_magic_quotes_gpc() -> fix for php8 (thanks @etlam) 12 | - "Prepare" -> throw exception on sql prepare errors 13 | - more fixes for php8 14 | 15 | 8.2.10 (2021-11-12) 16 | 17 | - "Helper" -> fix for php8 18 | 19 | 8.2.9 (2020-08-23) 20 | 21 | - "DB" -> ignore more invalid mysql warnings (fix typo) 22 | 23 | 8.2.8 (2020-08-23) 24 | 25 | - "DB" -> ignore more invalid mysql warnings 26 | - "DB" -> sync behavior of "beginTransaction()" for mysqli & doctrine 27 | 28 | 8.2.7 (2020-02-23) 29 | 30 | - update "symfony/property-access" 31 | 32 | 8.2.6 (2020-02-05) 33 | 34 | - "DB" -> check for mysql warnings 35 | 36 | 8.2.5 (2020-01-19) 37 | 38 | - fix "Select IN causing issues" (#46) 39 | 40 | 8.2.4 (2019-11-29) 41 | 42 | - update dependencies 43 | 44 | 8.2.3 (2019-11-28) 45 | 46 | - fix error handling for "MySQL server has gone away" 47 | 48 | 8.2.2 (2019-11-24) 49 | 50 | - fix php notice -> "Undefined index: file" 51 | 52 | 8.2.1 (2019-11-21) 53 | 54 | - fix caching of query results 55 | 56 | 8.2.0 (2019-11-21) 57 | 58 | - use "yield" and "references" to save more memory 59 | 60 | 8.1.0 (2019-11-18) 61 | 62 | - fix errors reported by phpstan (level 7) 63 | - "Result" -> add more log + debug information 64 | - "DB" -> add support for "flags" 65 | 66 | 67 | 8.0.6 (2019-10-08) 68 | 69 | - "Result" -> fix DECIMAL is a "string"-format for numbers 70 | 71 | 72 | 8.0.5 (2019-07-31) 73 | 74 | - "Prepare" -> fix type compatibility with "mysqli_stmt" 75 | 76 | 77 | 8.0.4 (2019-07-25) 78 | 79 | - update dependencies 80 | - extend "Debug::logger" 81 | - fix errors reported by phpstan (level 7) 82 | 83 | 84 | 8.0.3 (2019-02-24) 85 | 86 | - "DB" -> replace "DateTime" check to "DateTimeInterface" 87 | - update "simple-cache" v3.2 -> v4.0 88 | 89 | 90 | 8.0.2 (2019-02-13) 91 | 92 | - fix php warning, if the db config contains multi-array 93 | 94 | 95 | 8.0.1 (2019-01-11) 96 | 97 | - fix usage of "Arrayy" 98 | 99 | 100 | 8.0.0 (2018-12-21) 101 | 102 | - move "Active Record"-classes into a separate repository 103 | -> https://github.com/voku/simple-active-record 104 | 105 | 106 | 7.4.1 (2018-11-23) 107 | 108 | - add support for "+" and "-" for DB->update() 109 | 110 | 111 | 7.4.0 (2018-11-11) 112 | 113 | - add support for PDO connection as parent driver (via Doctrine/DBAL) 114 | 115 | 116 | 7.3.0 (2018-11-03) 117 | 118 | - simple active record -> use "@property" phpdoc type check via Arrayy 119 | 120 | 121 | 7.2.1 (2018-06-19) 122 | 123 | - optimize for PHP >= 7.0 124 | - fix doc / examples for the simple active record 125 | - add more tests 126 | 127 | 128 | 7.2.0 (2018-06-04) 129 | 130 | - add support for Doctrine/DBAL as parent driver 131 | 132 | 133 | 7.1.4 (2018-04-28) 134 | 135 | - DB->_parseQueryParamsByName() is private now (only internal usage) 136 | 137 | 138 | 7.1.3 (2018-04-27) 139 | 140 | - optimize the "escape" function 141 | - do not trim the input string 142 | 143 | 144 | 7.1.2 (2018-04-27) 145 | 146 | - optimize performance for the query builder 147 | 148 | 149 | 7.1.1 (2018-02-13) 150 | 151 | - "DB" -> implement "re_connect" for "DB::getInstance()" 152 | 153 | 154 | 7.1.0 (2018-01-21) 155 | 156 | - "define constants for default_result_type" 157 | - add usage of "yield" via "Result->fetchAllYield()" 158 | 159 | 160 | 7.0.2 (2018-01-07) 161 | 162 | - use static cache for the temporary parse-key 163 | 164 | 165 | 7.0.1 (2018-01-02) 166 | 167 | - fix for "DB->query()" + '?' (old sql-style) 168 | 169 | 170 | 7.0.0 (2017-12-23) 171 | 172 | - update "Portable UTF8" from v4 -> v5 173 | 174 | -> this is a breaking change without API-changes - but the requirement from 175 | "Portable UTF8" has been changed (it no longer requires all polyfills from Symfony) 176 | 177 | 178 | 6.1.1 (2017-12-21) 179 | 180 | - "DB" -> simplify -> !is_array(val) { \[val\] } to val = (array)val 181 | 182 | 183 | 6.1.0 (2017-12-14) 184 | 185 | - add "DB->setConfigExtra()" 186 | 187 | 188 | 6.0.3 (2017-12-03) 189 | 190 | - fix logging + PHP 7.0 191 | 192 | 193 | 6.0.2 (2017-12-03) 194 | 195 | - update "voku/simple-cache" 196 | 197 | 198 | 6.0.1 (2017-12-01) 199 | - fix declaration of voku\db\Prepare::prepare (mysqli_stmt::prepare) 200 | - micro optimization 201 | - update phpunit-config 202 | 203 | 204 | 6.0.0 (2017-11-13) 205 | - "php": ">=7.0" 206 | * drop support for PHP < 7.0 207 | * use "strict_types" 208 | 209 | 210 | 5.4.8 (2017-12-20) 211 | 212 | - add "setConfigExtra()" (backport) 213 | 214 | 5.4.7 (2017-10-15) 215 | 216 | - improve "DB->close()" + tests 217 | 218 | 5.4.6 (2017-10-14) 219 | 220 | - fix + test for double connection close 221 | 222 | 5.4.5 (2017-10-11) 223 | 224 | - "ActiveRecord" -> fix return values from DB-class 225 | 226 | 5.4.4 (2017-09-28) 227 | 228 | - fix "insert()", "delete()", etc. with empty string input 229 | 230 | 5.4.3 (2017-09-28) 231 | 232 | - fix -> DB->escape() (same fix as for "DB->secure()") 233 | 234 | 5.4.2 (2017-09-15) 235 | 236 | - fix -> DB->secure() 237 | 238 | 5.4.1 (2017-09-08) 239 | 240 | - update php-docs 241 | - DB->set_convert_null_to_empty_string(false) -> NULL === 'NULL' 242 | 243 | 5.4.0 (2017-09-03) 244 | 245 | - update docs + examples 246 | - fix code-style 247 | - add ActiveRecord::fetchEmpty() 248 | 249 | 5.3.1 (2017-09-03) 250 | 251 | - update docs + examples 252 | - DB->set_convert_null_to_empty_string() -> is deprecated 253 | 254 | 5.3.0 (2017-09-03) 255 | 256 | - "ActiveRecord" -> add more fetch methods 257 | - "ActiveRecord" -> fix "resetDirty()" 258 | 259 | 5.2.1 (2017-09-03) 260 | 261 | - DB->table_exists() && DB->num_rows() -> fix + tests 262 | 263 | 5.2.0 (2017-09-02) 264 | 265 | - add "ActiveRecord"-class + doc + tests 266 | 267 | 5.1.0 (2017-08-26) 268 | 269 | - SSL connection for mysqli 270 | - fix custom-exceptions 271 | - fix transaction-handling 272 | - add new parameter via ":column" (_parseQueryParamsByName) 273 | - foreach for the result-object 274 | - __invoke for the "DB"-class -> e.g.: $result = $db('SELECT ...'); 275 | - __invoke for the "Result"-class -> e.g.: $result(function ($result) use (&$foo) { } 276 | - add DB->transact() + doc + tests 277 | - add DB->select_db() 278 | - the "Result"-class now implements "\Countable, \SeekableIterator, \ArrayAccess" interfaces 279 | - add Result->fetchCallable() + doc + tests 280 | 281 | 5.0.0 (2017-08-10) 282 | 283 | - update vendor 284 | 285 | 5.0.0 (2017-07-22) 286 | 287 | - throw custom-exceptions and throw them only if needed 288 | 289 | - DBConnectException: will be thrown from DB->connect() 290 | - DBGoneAwayException: will be thrown by "server has gone away"-error 291 | - QueryException: will be thrown by "query"-error 292 | 293 | 4.4.3 (2017-05-22) 294 | 295 | - fix return types of "fetchArray()" / "fetchArrayy()" 296 | 297 | 4.4.2 (2017-05-21) 298 | 299 | - fix return of "DB->ping()" -> if there isn't a link to the db 300 | 301 | 4.4.1 (2017-05-05) 302 | 303 | - add caching for "Helper::phoneticSearch()" + tests 304 | 305 | 4.4.0 (2017-04-10) 306 | 307 | - use a new version of "Arrayy" (vendor) 308 | - use "DB->_parseArrayPair()" in te "Helper"-Class 309 | - use the "phonetic-algorithms" in the database-layer 310 | - only internal re-naming of static variable 311 | - update / fix php-doc 312 | 313 | 4.3.1 (2017-04-03) 314 | 315 | - add the "$databaseName"-parameter to "Helper::copyTableRow()" and "Helper::getDbFields()" 316 | 317 | 4.3.0 (2017-03-31) 318 | 319 | - add "Result->fetchAllColumn()" 320 | - add new parameter for "Result->fetchColumn()" 321 | - fix usage of optional "$database"-parameter for $db->replace() 322 | 323 | 4.2.6 (2017-03-29) 324 | 325 | - fix usage of optional "$database"-parameter for $db->insert() / $db->select() / $db->update() 326 | 327 | 4.2.5 (2017-03-24) 328 | 329 | - fix "DB->quote_string()" -> now we can also process already backtick-quoted strings 330 | - simplify some "if"-statements 331 | 332 | 4.2.4 (2017-03-15) 333 | 334 | - optimize "DB->escape()" 335 | 336 | 4.2.3 (2017-03-09) 337 | 338 | - prepare for PHP7 and "declare(strict_types=1);" 339 | - use new version of "Portable-UTF8"-vendor via composer.json 340 | 341 | 4.2.2 (2017-01-23) 342 | 343 | - fix "Result->cast()" for PHP 5.3 without mysqlnd 344 | 345 | 4.2.1 (2017-01-10) 346 | 347 | - fix "Helper::getDbFields()" for database+table name 348 | 349 | 4.2.0 (2017-01-09) 350 | 351 | - use new version of the "Arrayy"-class (vendor) 352 | 353 | 4.1.2 (2016-12-22) 354 | 355 | - use "UTF8::json_encode()" in the "Result"-object 356 | - add more alias-functions for "Arrayy"-usage 357 | - add more php-docs for the "Result"-object 358 | 359 | 4.1.0 (2016-12-21) 360 | 361 | - add "Prepare->execute_raw()" -> without debugging or logging 362 | 363 | 4.0.1 (2016-12-19) 364 | 365 | - use parameter (array) check for DB->update() / DB->insert() / DB->replace() 366 | - optimize memory usage from Helper->copyTableRow() 367 | - simplify some code 368 | 369 | 4.0.0 (2016-12-16) 370 | 371 | - edit "Prepare->execute()" -> the method will now return an "Result"-object for SELECT queries 372 | 373 | WARNING: If you already use "Prepare->execute()" for SELECT-queries, you need to change your code, 374 | because the method will now return an "Result"-object instead of true on success. 375 | 376 | 3.0.4 (2016-11-02) 377 | 378 | - fixed "_parseQueryParams()" (e.g. $0 should not replaced by php) 379 | 380 | 3.0.3 (2016-09-01) 381 | 382 | - fixed "copyTableRow()" (do not escape non selected data) 383 | 384 | 3.0.2 (2016-08-18) 385 | 386 | - use "utf8mb4" if it's supported 387 | 388 | 3.0.1 (2016-08-15) 389 | 390 | - fixed usage of (float) 391 | 392 | 3.0.0 (2016-08-15) 393 | ------------------ 394 | 395 | - merge "secure()" and "escape()" methods 396 | - convert "DateTime"-object to "DateTime"-string via "escape()" 397 | - check magic method "__toString" for "escape()"-input 398 | 399 | WARNING: Use "set_convert_null_to_empty_string(true)" to be compatible with the <= 2.0.x tags. 400 | 401 | 2.0.5/6 (2016-08-12) 402 | ------------------ 403 | 404 | - use new version of "portable-utf8" (3.0) 405 | 406 | 2.0.4 (2016-07-20) 407 | ------------------ 408 | 409 | - use "assertSame" instead of "assertEquals" (PhpUnit) 410 | - fix "DB->escape()" usage with arrays 411 | 412 | 2.0.3 (2016-07-11) 413 | ------------------ 414 | 415 | - fix used of "MYSQLI_OPT_INT_AND_FLOAT_NATIVE" 416 | -> "Type: Notice Message: Use of undefined constant MYSQLI_OPT_INT_AND_FLOAT_NATIVE" 417 | 418 | 419 | 2.0.2 (2016-07-11) 420 | ------------------ 421 | 422 | - fixed return from "DB->qry()" 423 | -> e.g. if an update-query updated zero rows, then we return "0" instead of "true" now 424 | 425 | 426 | 2.0.1 (2016-07-11) 427 | ------------------ 428 | 429 | - fixed return from "DB->query()" and "Prepare->execute()" 430 | -> e.g. if an update-query updated zero rows, then we return "0" instead of "true" now 431 | 432 | 433 | 2.0.0 (2016-07-11) 434 | ------------------ 435 | 436 | INFO: There was no breaking API changes, so you can easily upgrade from 1.x. 437 | 438 | - use "MYSQLI_OPT_INT_AND_FLOAT_NATIVE" + fallback 439 | - fixed return statements from "DB"-Class e.g. from "query()", "execSQL()" 440 | - don't use "UTF8::html_entity_decode()" by default 441 | - added "Prepare->bind_param_debug()" for debugging and logging prepare statements 442 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jonathan Tavares, Lars Moelleken 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/voku/simple-mysqli) 2 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fvoku%2Fsimple-mysqli?ref=badge_shield) 3 | [](https://coveralls.io/github/voku/simple-mysqli?branch=master) 4 | [](https://www.codacy.com/app/voku/simple-mysqli) 5 | [](https://packagist.org/packages/voku/simple-mysqli) 6 | [](https://packagist.org/packages/voku/simple-mysqli) 7 | [](https://packagist.org/packages/voku/simple-mysqli) 8 | [](https://www.paypal.me/moelleken) 9 | [](https://www.patreon.com/voku) 10 | 11 | # :gem: Simple MySQLi Class 12 | 13 | This is a simple MySQL Abstraction Layer compatible with PHP 7+ & PHP 8.0 that provides a simple 14 | and _secure_ interaction with your database using mysqli_* functions at 15 | its core. This is perfect for small scale applications such as cron jobs, 16 | facebook canvas campaigns or micro frameworks or sites. 17 | 18 | You can also use the :ring: ["Simple Active Record"](https://github.com/voku/simple-active-record)-class, it's based on this db class and add some OOP syntax. But please inform you about "Active Record" vs "Data Mapper" before you use it. 19 | 20 | 21 | ### Get "Simple MySQLi" 22 | 23 | You can download it from here, or require it using [composer](https://packagist.org/packages/voku/simple-mysqli). 24 | ```json 25 | { 26 | "require": { 27 | "voku/simple-mysqli": "8.*" 28 | } 29 | } 30 | ``` 31 | 32 | ### Install via "composer require" 33 | ```shell 34 | composer require voku/simple-mysqli 35 | ``` 36 | 37 | * [Starting the driver](#starting-the-driver) 38 | * [Multiton && Singleton](#multiton--singleton) 39 | * [Doctrine/DBAL as parent driver](#doctrinedbal-as-parent-driver) 40 | * [Using the "DB"-Class](#using-the-db-class) 41 | * [Selecting and retrieving data from a table](#selecting-and-retrieving-data-from-a-table) 42 | * [Inserting data on a table](#inserting-data-on-a-table) 43 | * [Binding parameters on queries](#binding-parameters-on-queries) 44 | * [Transactions](#transactions) 45 | * [Using the "Result"-Class](#using-the-result-class) 46 | * [Fetching all data](#fetching-all-data) 47 | * [Fetching database-table-fields](#fetching-database-table-fields) 48 | * [Fetching + Callable](#fetching--callable) 49 | * [Fetching + Transpose](#fetching--transpose) 50 | * [Fetching + Pairs](#fetching--pairs) 51 | * [Fetching + Groups](#fetching--groups) 52 | * [Fetching + first](#fetching--first) 53 | * [Fetching + last](#fetching--last) 54 | * [Fetching + slice](#fetching--slice) 55 | * [Fetching + map](#fetching--map) 56 | * [Fetching + aliases](#fetching--aliases) 57 | * [Fetching + Iterations](#fetching--iterations) 58 | * [Using the "Prepare"-Class](#using-the-prepare-class) 59 | * [INSERT-Prepare-Query (example)](#insert-prepare-query-example) 60 | * [SELECT-Prepare-Query (example)](#select-prepare-query-example) 61 | * [Logging and Errors](#logging-and-errors) 62 | * [Changelog](#changelog) 63 | 64 | 65 | ### Starting the driver 66 | ```php 67 | use voku\db\DB; 68 | 69 | require_once 'composer/autoload.php'; 70 | 71 | $db = DB::getInstance('yourDbHost', 'yourDbUser', 'yourDbPassword', 'yourDbName'); 72 | 73 | // example 74 | // $db = DB::getInstance('localhost', 'root', '', 'test'); 75 | ``` 76 | 77 | ### Multiton && Singleton 78 | 79 | You can use ```DB::getInstance()``` without any parameters and you will get your (as "singleton") first initialized connection. Or you can change the parameter and you will create an new "multiton"-instance which works like an singleton, but you need to use the same parameters again, otherwise (without the same parameter) you will get an new instance. 80 | 81 | ### Doctrine/DBAL as parent driver 82 | ```php 83 | use voku\db\DB; 84 | 85 | require_once 'composer/autoload.php'; 86 | 87 | $connectionParams = [ 88 | 'dbname' => 'yourDbName', 89 | 'user' => 'yourDbUser', 90 | 'password' => 'yourDbPassword', 91 | 'host' => 'yourDbHost', 92 | 'driver' => 'mysqli', // 'pdo_mysql' || 'mysqli' 93 | 'charset' => 'utf8mb4', 94 | ]; 95 | $config = new \Doctrine\DBAL\Configuration(); 96 | $doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection( 97 | $connectionParams, 98 | $config 99 | ); 100 | $doctrineConnection->connect(); 101 | 102 | $db = DB::getInstanceDoctrineHelper($doctrineConnection); 103 | ``` 104 | 105 | ## Using the "DB"-Class 106 | 107 | There are numerous ways of using this library, here are some examples of the most common methods. 108 | 109 | #### Selecting and retrieving data from a table 110 | 111 | ```php 112 | use voku\db\DB; 113 | 114 | $db = DB::getInstance(); 115 | 116 | $result = $db->query("SELECT * FROM users"); 117 | $users = $result->fetchAll(); 118 | ``` 119 | 120 | But you can also use a method for select-queries: 121 | 122 | ```php 123 | $db->select(string $table, array $where); // generate an SELECT query 124 | ``` 125 | 126 | Example: SELECT 127 | ```php 128 | $where = [ 129 | 'page_type =' => 'article', 130 | 'page_type NOT LIKE' => '%öäü123', 131 | 'page_id >=' => 2, 132 | ]; 133 | $articles = $db->select('page', $where); 134 | 135 | echo 'There are ' . count($articles) . ' article(s):' . PHP_EOL; 136 | 137 | foreach ($articles as $article) { 138 | echo 'Type: ' . $article['page_type'] . PHP_EOL; 139 | echo 'ID: ' . $article['page_id'] . PHP_EOL; 140 | } 141 | ``` 142 | 143 | Here is a list of connectors for the "WHERE"-array: 144 | 'NOT', 'IS', 'IS NOT', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'LIKE', 'NOT LIKE', '>', '<', '>=', '<=', '<>', '+', '-' 145 | 146 | INFO: use an array as $value for "[NOT] IN" and "[NOT] BETWEEN" 147 | 148 | INFO: use + / - in the value not in the key of the $data 149 | 150 | Example: UPDATE with "page_template = page_template + 1" 151 | ```php 152 | $where = [ 153 | 'page_type LIKE' => '%foo', 154 | 'page_type NOT LIKE' => 'bar', 155 | ]; 156 | $data = [ 157 | 'page_template' => ['page_template +' => 1], 158 | 'page_type' => 'lall', 159 | ]; 160 | $resultSelect = $db->update('page', $data, $where); 161 | ``` 162 | 163 | Example: SELECT with "NOT IN" 164 | ```php 165 | $where = [ 166 | 'page_type NOT IN' => [ 167 | 'foo', 168 | 'bar' 169 | ], 170 | 'page_id >' => 2, 171 | ]; 172 | $resultSelect = $db->select('page', $where); 173 | ``` 174 | 175 | Example: SELECT with Cache 176 | ```php 177 | $resultSelect = $db->execSQL("SELECT * FROM users", true, 3600); 178 | ``` 179 | 180 | The result (via $result->fetchAllArray()) is only cached for 3600s when the query was a SELECT statement, otherwise you get the default result from the ```$db->query()``` function. 181 | 182 | #### Inserting data on a table 183 | 184 | to manipulate tables you have the most important methods wrapped, 185 | they all work the same way: parsing arrays of key/value pairs and forming a safe query 186 | 187 | the methods are: 188 | ```php 189 | $db->insert( string $table, array $data ); // generate an INSERT query 190 | $db->replace( string $table, array $data ); // generate an REPLACE query 191 | $db->update( string $table, array $data, array $where ); // generate an UPDATE query 192 | $db->delete( string $table, array $where ); // generate a DELETE query 193 | ``` 194 | 195 | All methods will return the resulting `mysqli_insert_id()` or true/false depending on context. 196 | The correct approach if to always check if they executed as success is always returned 197 | 198 | Example: DELETE 199 | ```php 200 | $deleteArray = ['user_id' => 9]; 201 | $ok = $db->delete('users', $deleteArray); 202 | if ($ok) { 203 | echo "user deleted!"; 204 | } else { 205 | echo "can't delete user!"; 206 | } 207 | ``` 208 | 209 | **note**: all parameter values are sanitized before execution, you don\'t have to escape values beforehand. 210 | 211 | Example: INSERT 212 | ```php 213 | $insertArray = [ 214 | 'name' => "John", 215 | 'email' => "johnsmith@email.com", 216 | 'group' => 1, 217 | 'active' => true, 218 | ]; 219 | $newUserId = $db->insert('users', $insertArray); 220 | if ($newUserId) { 221 | echo "new user inserted with the id $new_user_id"; 222 | } 223 | ``` 224 | 225 | Example: REPLACE 226 | ```php 227 | $replaceArray = [ 228 | 'name' => 'lars', 229 | 'email' => 'lars@moelleken.org', 230 | 'group' => 0 231 | ]; 232 | $tmpId = $db->replace('users', $replaceArray); 233 | ``` 234 | 235 | #### Binding parameters on queries 236 | 237 | Binding parameters is a good way of preventing mysql injections as the parameters are sanitized before execution. 238 | 239 | ```php 240 | $sql = "SELECT * FROM users 241 | WHERE id_user = :id_user 242 | AND active = :active 243 | LIMIT 1 244 | "; 245 | $result = $db->query($sql, ['id_user' => 11, 'active' => 1]); 246 | if ($result) { 247 | $user = $result->fetchArray(); 248 | print_r($user); 249 | } else { 250 | echo "user not found"; 251 | } 252 | ``` 253 | 254 | #### Transactions 255 | 256 | Use `begin()`, `commit()`, and `rollback()` to manage transactions: 257 | 258 | ```php 259 | $db->beginTransaction(); 260 | 261 | $db->query( 262 | 'UPDATE `users` SET `foo` = :foo WHERE id = :id', 263 | ['foo' => 100, 'id' => 1] 264 | ); 265 | $db->query( 266 | 'UPDATE `users_noop` SET `foo` = :foo WHERE id = :id', 267 | ['foo' => 100, 'id' => 2] 268 | ); 269 | 270 | $db->endTransaction(); 271 | ``` 272 | 273 | Any SQL errors between `begin()` and `commit()` will yield a `RuntimeException`. 274 | 275 | You can also use the `DB->transact()` method. The following is equivalent 276 | to the above: 277 | 278 | ```php 279 | $db->transact(function($db) { 280 | $db->query( 281 | 'UPDATE `users` SET `foo` = :foo WHERE id = :id', 282 | ['foo' => 100, 'id' => 1] 283 | ); 284 | $db->query( 285 | 'UPDATE `users_noop` SET `foo` = :foo WHERE id = :id', 286 | ['foo' => 100, 'id' => 2] 287 | ); 288 | }); 289 | ``` 290 | 291 | ### Using the "Result"-Class 292 | 293 | After executing a `SELECT` query you receive a `Result` object that will help you manipulate the resultant data. 294 | there are different ways of accessing this data, check the examples bellow: 295 | 296 | #### Fetching all data 297 | 298 | ```php 299 | $result = $db->query("SELECT * FROM users"); 300 | $allUsers = $result->fetchAll(); 301 | ``` 302 | Fetching all data works as Result::RESULT_TYPE_* the `fetchAll()` and `fetch()` method will return the default based on the `$_default_result_type` config. 303 | Other methods are: 304 | 305 | ```php 306 | $row = $result->fetch(); // fetch an single result row as defined by the config (array, object or Arrayy) 307 | $row = $result->fetchArray(); // fetch an single result row as array 308 | $row = $result->fetchArrayy(); // fetch an single result row as Arrayy object 309 | $row = $result->fetchObject(); // fetch an single result row as object 310 | $row = $result->fetchYield(); // fetch an single result row as Generator 311 | 312 | $data = $result->fetchAll(); // fetch all result data as defined by the config (array, object or Arrayy) 313 | $data = $result->fetchAllArray(); // fetch all result data as array 314 | $data = $result->fetchAllArrayy(); // fetch all result data as Array object 315 | $data = $result->fetchAllObject(); // fetch all result data as object 316 | $data = $result->fetchAllYield(); // fetch all result data as Generator 317 | 318 | $data = $result->fetchColumn(string $column, bool $skipNullValues); // fetch a single column as string 319 | $data = $result->fetchAllColumn(string $column, bool $skipNullValues); // fetch a single column as an 1-dimension array 320 | 321 | $data = $result->fetchArrayPair(string $key, string $value); // fetch data as a key/value pair array 322 | ``` 323 | 324 | #### Fetching database-table-fields 325 | 326 | Returns rows of field information in a result set: 327 | 328 | ```php 329 | $fields = $result->fetchFields(); 330 | ``` 331 | 332 | Pass `true` as argument if you want each field information returned as an 333 | associative array instead of an object. The default is to return each as an 334 | object, exactly like the `mysqli_fetch_fields` function. 335 | 336 | #### Fetching + Callable 337 | 338 | Fetches a row or a single column within a row: 339 | 340 | ```php 341 | $data = $result->fetch($row_number, $column); 342 | ``` 343 | 344 | This method forms the basis of all fetch_ methods. All forms of fetch_ advances 345 | the internal row pointer to the next row. `null` will be returned when there are 346 | no more rows to be fetched. 347 | 348 | #### Fetching + Transpose 349 | 350 | Returns all rows at once, transposed as an array of arrays: 351 | 352 | ```php 353 | $plan_details = $plans->fetchTranspose(); 354 | ``` 355 | 356 | Transposing a result set of X rows each with Y columns will result in an array 357 | of Y rows each with X columns. 358 | 359 | Pass a column name as argument to return each column as an associative array 360 | with keys taken from values of the provided column. If not provided, the keys 361 | will be numeric starting from zero. 362 | 363 | e.g.: 364 | ```php 365 | $transposedExample = [ 366 | 'title' => [ 367 | 1 => 'Title #1', 368 | 2 => 'Title #2', 369 | 3 => 'Title #3', 370 | ], 371 | ); 372 | ``` 373 | 374 | #### Fetching + Pairs 375 | 376 | Returns all rows at once as key-value pairs using the column in the first 377 | argument as the key: 378 | 379 | ```php 380 | $countries = $result->fetchPairs('id'); 381 | ``` 382 | 383 | Pass a column name as the second argument to only return a single column as the 384 | value in each pair: 385 | 386 | ```php 387 | $countries = $result->fetchPairs('id', 'name'); 388 | 389 | /* 390 | [ 391 | 1 => 'Title #1', 392 | 2 => 'Title #2', 393 | 3 => 'Title #3', 394 | ] 395 | */ 396 | ``` 397 | 398 | #### Fetching + Groups 399 | 400 | Returns all rows at once as a grouped array: 401 | 402 | ```php 403 | $students_grouped_by_gender = $result->fetchGroups('gender'); 404 | ``` 405 | 406 | Pass a column name as the second argument to only return single columns as the 407 | values in each groups: 408 | 409 | ```php 410 | $student_names_grouped_by_gender = $result->fetchGroups('gender', 'name'); 411 | ``` 412 | 413 | #### Fetching + first 414 | 415 | Returns the first row element from the result: 416 | 417 | ```php 418 | $first = $result->first(); 419 | ``` 420 | 421 | Pass a column name as argument to return a single column from the first row: 422 | 423 | ```php 424 | $name = $result->first('name'); 425 | ``` 426 | 427 | #### Fetching + last 428 | 429 | Returns the last row element from the result: 430 | 431 | ```php 432 | $last = $result->last(); 433 | ``` 434 | 435 | Pass a column name as argument to return a single column from the last row: 436 | 437 | ```php 438 | $name = $result->last('name'); 439 | ``` 440 | 441 | #### Fetching + slice 442 | 443 | Returns a slice of rows from the result: 444 | 445 | ```php 446 | $slice = $result->slice(1, 10); 447 | ``` 448 | 449 | The above will return 10 rows skipping the first one. The first parameter is the 450 | zero-based offset; the second parameter is the number of elements; the third 451 | parameter is a boolean value to indicate whether to preserve the keys or not 452 | (optional and defaults to false). This methods essentially behaves the same as 453 | PHP's built-in `array_slice()` function. 454 | 455 | #### Fetching + map 456 | 457 | Sets a mapper callback function that's used inside the `Result->fetchCallable()` method: 458 | 459 | ```php 460 | $result->map(function($row) { 461 | return (object) $row; 462 | }); 463 | $object = $result->fetchCallable(0); 464 | ``` 465 | 466 | The above example will map one row (0) from the result into a 467 | object. Set the mapper callback function to null to disable it. 468 | 469 | #### Fetching + aliases 470 | ```php 471 | $db->get() // alias for $db->fetch(); 472 | $db->getAll() // alias for $db->fetchAll(); 473 | $db->getObject() // alias for $db->fetchAllObject(); 474 | $db->getArray() // alias for $db->fetchAllArray(); 475 | $db->getArrayy() // alias for $db->fetchAllArrayy(); 476 | $db->getYield() // alias for $db->fetchAllYield(); 477 | $db->getColumn($key) // alias for $db->fetchColumn($key); 478 | ``` 479 | 480 | #### Fetching + Iterations 481 | To iterate a result-set you can use any fetch() method listed above. 482 | 483 | ```php 484 | $result = $db->select('users'); 485 | 486 | // using while 487 | while ($row = $result->fetch()) { 488 | echo $row->name; 489 | echo $row->email; 490 | } 491 | 492 | // using foreach (via "fetchAllObject()") 493 | foreach($result->fetchAllObject() as $row) { 494 | echo $row->name; 495 | echo $row->email; 496 | } 497 | 498 | // using foreach (via "Result"-object) 499 | foreach($result as $row) { 500 | echo $row->name; 501 | echo $row->email; 502 | } 503 | 504 | // using foreach (via "Generator"-object) 505 | foreach($result->fetchAllYield() as $row) { 506 | echo $row->name; 507 | echo $row->email; 508 | } 509 | 510 | // INFO: "while + fetch()" and "fetchAllYield()" will use less memory that "foreach + "fetchAllObject()", because we will fetch each result entry seperatly 511 | ``` 512 | 513 | #### Executing Multi Queries 514 | To execute multiple queries you can use the ```$db->multi_query()``` method. You can use multiple queries separated by "```;```". 515 | 516 | Return-Types: 517 |
The error message.
153 | * @param bool|null $force_exception_after_error154 | * If you use default "null" here, then the behavior depends 155 | * on "$this->exit_on_error (default: true)". 156 | *
157 | * 158 | * @throws QueryException 159 | * 160 | * @return void 161 | */ 162 | public function displayError($error, $force_exception_after_error = null) 163 | { 164 | $fileInfo = $this->getFileAndLineFromSql(); 165 | 166 | $log = '[' . \date('Y-m-d H:i:s') . ']: SQL-Error: ' . $error . ' | Trace: ' . $fileInfo['path'] . '
190 | Error:' . $error . '
191 |
192 | Trace: ' . $fileInfo['path'] . '
193 |
194 | Will return false, if no logging was used.
297 | */ 298 | public function logQuery($sql, $duration, $results, bool $sql_error = false) 299 | { 300 | $logLevelUse = \strtolower($this->logger_level); 301 | 302 | if ( 303 | $sql_error === false 304 | && 305 | ($logLevelUse !== 'trace' && $logLevelUse !== 'debug') 306 | ) { 307 | return false; 308 | } 309 | 310 | // set log-level 311 | $logLevel = $logLevelUse; 312 | if ($sql_error === true) { 313 | $logLevel = 'error'; 314 | } 315 | 316 | // 317 | // logging 318 | // 319 | 320 | $traceStringExtra = ''; 321 | if ($logLevelUse === 'trace') { 322 | $tmpLink = $this->_db->getLink(); 323 | if ($tmpLink && $tmpLink instanceof \mysqli) { 324 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 325 | $traceStringExtra = @\mysqli_info($tmpLink); 326 | if ($traceStringExtra) { 327 | $traceStringExtra = ' | info => ' . $traceStringExtra; 328 | } 329 | } 330 | 331 | $traceStringExtra = ' | results => ' . \print_r($results, true) . $traceStringExtra; 332 | } 333 | 334 | static $SLOW_QUERY_WARNING = null; 335 | static $QUERY_LOG_FILE_INFO = []; 336 | 337 | $queryStatus = ''; 338 | if ($duration >= $this->slowQueryTimeWarning) { 339 | $queryStatus = ' WARN (DURATION) '; 340 | } 341 | if ($duration >= $this->slowQueryTimeError) { 342 | $queryStatus = ' ERROR (DURATION) '; 343 | } 344 | 345 | $fileInfo = $this->getFileAndLineFromSql(); 346 | $cacheKey = \md5($fileInfo['path']); 347 | if (empty($QUERY_LOG_FILE_INFO[$cacheKey])) { 348 | $QUERY_LOG_FILE_INFO[$cacheKey] = 0; 349 | } 350 | ++$QUERY_LOG_FILE_INFO[$cacheKey]; 351 | 352 | if ($QUERY_LOG_FILE_INFO[$cacheKey] >= $this->maxQueryRepeatWarning) { 353 | $queryStatus = ' WARN (REPEAT) '; 354 | } 355 | if ($QUERY_LOG_FILE_INFO[$cacheKey] >= $this->maxQueryRepeatError) { 356 | $queryStatus = ' ERROR (REPEAT) '; 357 | } 358 | 359 | $queryLog = '[' . \date('Y-m-d H:i:s') . ']: ' . $queryStatus . ' Duration: SQL::::DURATION-START' . \round($duration, 5) . 'SQL::::DURATION-END | Repeat: ' . $QUERY_LOG_FILE_INFO[$cacheKey] . ' | Host: ' . $this->_db->getConfig()['hostname'] . ' | Trace: ' . $fileInfo['path'] . ' | SQL: SQL::::QUERY-START ' . \str_replace("\n", '', $sql) . ' SQL::::QUERY-END' . $traceStringExtra . "\n"; 360 | 361 | return $this->logger([$logLevel, $queryLog, 'sql']); 362 | } 363 | 364 | /** 365 | * Wrapper-Function for a "Logger"-Class. 366 | * 367 | * INFO: 368 | * The "Logger"-ClassName is set by "$this->logger_class_name",Will return false, if no logging was used.
377 | */ 378 | public function logger(array $log) 379 | { 380 | // init 381 | $logMethod = ''; 382 | $logText = ''; 383 | $logType = 'sql'; 384 | $logClass = $this->logger_class_name; 385 | 386 | if (isset($log[0])) { 387 | $logMethod = $log[0]; 388 | } 389 | 390 | if (isset($log[1])) { 391 | $logText = $log[1]; 392 | } 393 | 394 | if (isset($log[2])) { 395 | $logType = $log[2]; 396 | } 397 | 398 | if ( 399 | $logClass 400 | && 401 | $logMethod 402 | && 403 | \class_exists($logClass) 404 | && 405 | \method_exists($logClass, $logMethod) 406 | ) { 407 | if (\method_exists($logClass, 'getInstance')) { 408 | return $logClass::getInstance()->{$logMethod}($logText, ['log_type' => $logType]); 409 | } 410 | 411 | return $logClass::$logMethod($logText, $logType); 412 | } 413 | 414 | return false; 415 | } 416 | 417 | /** 418 | * Send a error mail to the admin / dev. 419 | * 420 | * @param string $subject 421 | * @param string $htmlBody 422 | * @param int $priority 423 | * 424 | * @return void 425 | */ 426 | public function mailToAdmin($subject, $htmlBody, $priority = 3) 427 | { 428 | if (\function_exists('mailToAdmin')) { 429 | mailToAdmin($subject, $htmlBody, $priority); 430 | } else { 431 | if ($priority === 3) { 432 | $this->logger(['debug', $subject . ' | ' . $htmlBody]); 433 | } elseif ($priority > 3) { 434 | $this->logger(['error', $subject . ' | ' . $htmlBody]); 435 | } else { 436 | $this->logger(['info', $subject . ' | ' . $htmlBody]); 437 | } 438 | } 439 | } 440 | 441 | /** 442 | * @param bool $echo_on_error 443 | * 444 | * @return void 445 | */ 446 | public function setEchoOnError($echo_on_error) 447 | { 448 | $this->echo_on_error = (bool) $echo_on_error; 449 | } 450 | 451 | /** 452 | * @param bool $exit_on_error 453 | * 454 | * @return void 455 | */ 456 | public function setExitOnError($exit_on_error) 457 | { 458 | $this->exit_on_error = (bool) $exit_on_error; 459 | } 460 | 461 | /** 462 | * @param string $logger_class_name 463 | * 464 | * @return void 465 | */ 466 | public function setLoggerClassName($logger_class_name) 467 | { 468 | $this->logger_class_name = (string) $logger_class_name; 469 | } 470 | 471 | /** 472 | * @param string $logger_level 473 | * 474 | * @return void 475 | */ 476 | public function setLoggerLevel($logger_level) 477 | { 478 | $this->logger_level = (string) $logger_level; 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /src/voku/db/Helper.php: -------------------------------------------------------------------------------- 1 | getConfig(); 23 | \array_walk_recursive( 24 | $configOrig, 25 | static function ($k, $v) use (&$configTmp) { 26 | $configTmp[] = $v; 27 | $configTmp[] = $k; 28 | } 29 | ); 30 | 31 | return \implode('--', $configTmp); 32 | } 33 | 34 | /** 35 | * Optimize tables 36 | * 37 | * @param array $tables database table names 38 | * @param DB|null $dbConnectionUse null to get your first singleton instance.
39 | * 40 | * @return int 41 | */ 42 | public static function optimizeTables(array $tables = [], DB $dbConnection = null): int 43 | { 44 | if ($dbConnection === null) { 45 | $dbConnection = DB::getInstance(); 46 | } 47 | 48 | $optimized = 0; 49 | if (!empty($tables)) { 50 | foreach ($tables as $table) { 51 | $optimize = 'OPTIMIZE TABLE ' . $dbConnection->quote_string($table); 52 | $result = $dbConnection->query($optimize); 53 | if ($result) { 54 | $optimized++; 55 | } 56 | } 57 | } 58 | 59 | return $optimized; 60 | } 61 | 62 | /** 63 | * Repair tables 64 | * 65 | * @param array $tables database table names 66 | * @param DB|null $dbConnectionUse null to get your first singleton instance.
67 | * 68 | * @return int 69 | */ 70 | public static function repairTables(array $tables = [], DB $dbConnection = null): int 71 | { 72 | if ($dbConnection === null) { 73 | $dbConnection = DB::getInstance(); 74 | } 75 | 76 | $optimized = 0; 77 | if (!empty($tables)) { 78 | foreach ($tables as $table) { 79 | $optimize = 'REPAIR TABLE ' . $dbConnection->quote_string($table); 80 | $result = $dbConnection->query($optimize); 81 | if ($result) { 82 | $optimized++; 83 | } 84 | } 85 | } 86 | 87 | return $optimized; 88 | } 89 | 90 | /** 91 | * Check if "mysqlnd"-driver is used. 92 | * 93 | * @return bool 94 | */ 95 | public static function isMysqlndIsUsed(): bool 96 | { 97 | static $MYSQLND_IS_USED_CACHE = null; 98 | 99 | if ($MYSQLND_IS_USED_CACHE === null) { 100 | $MYSQLND_IS_USED_CACHE = ( 101 | \extension_loaded('mysqlnd') 102 | && 103 | \function_exists('mysqli_fetch_all') 104 | ); 105 | } 106 | 107 | return $MYSQLND_IS_USED_CACHE; 108 | } 109 | 110 | /** 111 | * Check if the current environment supports "utf8mb4". 112 | * 113 | * @param DB $dbConnection 114 | * 115 | * @return bool 116 | */ 117 | public static function isUtf8mb4Supported(DB $dbConnection = null): bool 118 | { 119 | /** 120 | * https://make.wordpress.org/core/2015/04/02/the-utf8mb4-upgrade/ 121 | * 122 | * - You’re currently using the utf8 character set. 123 | * - Your MySQL server is version 5.5.3 or higher (including all 10.x versions of MariaDB). 124 | * - Your MySQL client libraries are version 5.5.3 or higher. If you’re using mysqlnd, 5.0.9 or higher. 125 | * 126 | * INFO: utf8mb4 is 100% backwards compatible with utf8. 127 | */ 128 | if ($dbConnection === null) { 129 | $dbConnection = DB::getInstance(); 130 | } 131 | 132 | $server_version = self::get_mysql_server_version($dbConnection); 133 | $client_version = self::get_mysql_client_version($dbConnection); 134 | 135 | if ( 136 | $server_version >= 50503 137 | && 138 | ( 139 | ( 140 | self::isMysqlndIsUsed() 141 | && 142 | $client_version >= 50009 143 | ) 144 | || 145 | ( 146 | !self::isMysqlndIsUsed() 147 | && 148 | $client_version >= 50503 149 | ) 150 | ) 151 | 152 | ) { 153 | return true; 154 | } 155 | 156 | return false; 157 | } 158 | 159 | /** 160 | * A phonetic search algorithms for different languages. 161 | * 162 | * INFO: if you need better performance, please save the "voku\helper\Phonetic"-output into the DB and search for it 163 | * 164 | * @param string $searchString 165 | * @param string $searchFieldName 166 | * @param string $idFieldName 167 | * @param string $languageen, de, fr
168 | * @param string $table 169 | * @param array $whereArray 170 | * @param DB|null $dbConnectionuse null if you will use the current database-connection
171 | * @param string|null $databaseNameuse null if you will use the current database
172 | * @param bool $useCache use cache? 173 | * @param int $cacheTTL cache-ttl in seconds 174 | * 175 | * @return array 176 | */ 177 | public static function phoneticSearch( 178 | string $searchString, 179 | string $searchFieldName, 180 | string $idFieldName = null, 181 | string $language = 'de', 182 | string $table = '', 183 | array $whereArray = null, 184 | DB $dbConnection = null, 185 | string $databaseName = null, 186 | bool $useCache = false, 187 | int $cacheTTL = 3600 188 | ): array { 189 | // init 190 | $cacheKey = null; 191 | 192 | if ($dbConnection === null) { 193 | $dbConnection = DB::getInstance(); 194 | } 195 | 196 | if ($table === '') { 197 | $debug = new Debug($dbConnection); 198 | $debug->displayError('Invalid table name, table name in empty.', false); 199 | 200 | return []; 201 | } 202 | 203 | if ($idFieldName === null) { 204 | $idFieldName = 'id'; 205 | } 206 | 207 | if ($whereArray === null) { 208 | $whereArray = []; 209 | } 210 | 211 | $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND'); 212 | if ($whereSQL) { 213 | $whereSQL = 'AND ' . $whereSQL; 214 | } 215 | 216 | if ($databaseName) { 217 | $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.'; 218 | } 219 | 220 | // get the row 221 | $query = 'SELECT ' . $dbConnection->quote_string($searchFieldName) . ', ' . $dbConnection->quote_string($idFieldName) . ' 222 | FROM ' . $databaseName . $dbConnection->quote_string($table) . ' 223 | WHERE 1 = 1 224 | ' . $whereSQL . ' 225 | '; 226 | 227 | if ($useCache) { 228 | $cache = new \voku\cache\Cache(null, null, false, $useCache); 229 | $cacheKey = 'sql-phonetic-search-' . \md5($query); 230 | 231 | if ( 232 | $cache->getCacheIsReady() 233 | && 234 | $cache->existsItem($cacheKey) 235 | ) { 236 | return $cache->getItem($cacheKey); 237 | } 238 | } else { 239 | $cache = false; 240 | } 241 | 242 | $result = $dbConnection->query($query); 243 | 244 | if (!$result instanceof Result) { 245 | return []; 246 | } 247 | 248 | // make sure the row exists 249 | if ($result->num_rows <= 0) { 250 | return []; 251 | } 252 | 253 | $dataToSearchIn = []; 254 | /** @noinspection PhpAssignmentInConditionInspection */ 255 | while ($tmpArray = $result->fetchArray()) { 256 | $dataToSearchIn[$tmpArray[$idFieldName]] = $tmpArray[$searchFieldName]; 257 | } 258 | 259 | $phonetic = new \voku\helper\Phonetic($language); 260 | $return = $phonetic->phonetic_matches($searchString, $dataToSearchIn); 261 | 262 | // save into the cache 263 | if ( 264 | $cacheKey !== null 265 | && 266 | $useCache 267 | && 268 | $cache instanceof \voku\cache\Cache 269 | && 270 | $cache->getCacheIsReady() 271 | ) { 272 | $cache->setItem($cacheKey, $return, $cacheTTL); 273 | } 274 | 275 | return $return; 276 | } 277 | 278 | /** 279 | * A string that represents the MySQL client library version. 280 | * 281 | * @param DB $dbConnection 282 | * 283 | * @return string 284 | */ 285 | public static function get_mysql_client_version(DB $dbConnection = null): string 286 | { 287 | static $MYSQL_CLIENT_VERSION_CACHE = []; 288 | 289 | if ($dbConnection === null) { 290 | $dbConnection = DB::getInstance(); 291 | } 292 | 293 | $cacheKey = self::generateCacheKey($dbConnection); 294 | 295 | if (isset($MYSQL_CLIENT_VERSION_CACHE[$cacheKey])) { 296 | return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey]; 297 | } 298 | 299 | $doctrineConnection = $dbConnection->getDoctrineConnection(); 300 | if ($doctrineConnection) { 301 | $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection(); 302 | if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) { 303 | return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = $doctrineWrappedConnection->getAttribute(5); // 5 = PDO::ATTR_CLIENT_VERSION 304 | } 305 | } 306 | 307 | $mysqli_link = $dbConnection->getLink(); 308 | if (!$mysqli_link) { 309 | return ''; 310 | } 311 | 312 | /** @noinspection PhpParamsInspection - false-positiv | https://github.com/voku/simple-mysqli/issues/50 */ 313 | return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_client_version(); 314 | } 315 | 316 | /** 317 | * Returns a string representing the version of the MySQL server that the MySQLi extension is connected to. 318 | * 319 | * @param DB $dbConnection 320 | * 321 | * @return string 322 | */ 323 | public static function get_mysql_server_version(DB $dbConnection = null): string 324 | { 325 | static $MYSQL_SERVER_VERSION_CACHE = []; 326 | 327 | if ($dbConnection === null) { 328 | $dbConnection = DB::getInstance(); 329 | } 330 | 331 | $cacheKey = self::generateCacheKey($dbConnection); 332 | 333 | if (isset($MYSQL_SERVER_VERSION_CACHE[$cacheKey])) { 334 | return $MYSQL_SERVER_VERSION_CACHE[$cacheKey]; 335 | } 336 | 337 | $doctrineConnection = $dbConnection->getDoctrineConnection(); 338 | if ($doctrineConnection) { 339 | $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection(); 340 | if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) { 341 | return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) $doctrineWrappedConnection->getServerVersion(); 342 | } 343 | } 344 | 345 | $mysqli_link = $dbConnection->getLink(); 346 | if (!$mysqli_link) { 347 | return ''; 348 | } 349 | 350 | return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_server_version($mysqli_link); 351 | } 352 | 353 | /** 354 | * Return all db-fields from a table. 355 | * 356 | * @param string $table 357 | * @param bool $useStaticCache 358 | * @param DB|null $dbConnectionuse null if you will use the current database-connection
359 | * @param string|null $databaseNameuse null if you will use the current database
360 | * 361 | * @return array 362 | */ 363 | public static function getDbFields(string $table, bool $useStaticCache = true, DB $dbConnection = null, string $databaseName = null): array 364 | { 365 | static $DB_FIELDS_CACHE = []; 366 | 367 | // use the static cache 368 | if ( 369 | $useStaticCache 370 | && 371 | isset($DB_FIELDS_CACHE[$table]) 372 | ) { 373 | return $DB_FIELDS_CACHE[$table]; 374 | } 375 | 376 | // init 377 | $dbFields = []; 378 | 379 | if ($dbConnection === null) { 380 | $dbConnection = DB::getInstance(); 381 | } 382 | 383 | if ($table === '') { 384 | $debug = new Debug($dbConnection); 385 | $debug->displayError('Invalid table name, table name in empty.', false); 386 | 387 | return []; 388 | } 389 | 390 | if ($databaseName) { 391 | $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.'; 392 | } 393 | 394 | /** @var string $table */ 395 | $table = $dbConnection->escape($table); 396 | 397 | $sql = 'SHOW COLUMNS FROM ' . $databaseName . $table; 398 | $result = $dbConnection->query($sql); 399 | 400 | if ($result instanceof Result && $result->num_rows > 0) { 401 | foreach ($result->fetchAllArray() as $tmpResult) { 402 | $dbFields[] = $tmpResult['Field']; 403 | } 404 | } 405 | 406 | // add to static cache 407 | $DB_FIELDS_CACHE[$table] = $dbFields; 408 | 409 | return $dbFields; 410 | } 411 | 412 | /** 413 | * Copy row within a DB table and making updates to the columns. 414 | * 415 | * @param string $table 416 | * @param array $whereArray 417 | * @param array $updateArray 418 | * @param array $ignoreArray 419 | * @param DB|null $dbConnectionUse null to get your first singleton instance.
420 | * @param string|null $databaseNameuse null if you will use the current database
421 | * 422 | * @return false|int|string "int|string" (insert_id) by "INSERT / REPLACE"-queries262 | * The query, as a string. It must consist of a single SQL statement. 263 | *
264 | *265 | * You can include one or more parameter markers in the SQL statement by 266 | * embedding question mark (?) characters at the 267 | * appropriate positions. 268 | *
269 | *270 | * You should not add a terminating semicolon or \g 271 | * to the statement. 272 | *
273 | *274 | * The markers are legal only in certain places in SQL statements. 275 | * For example, they are allowed in the VALUES() list of an INSERT statement 276 | * (to specify column values for a row), or in a comparison with a column in 277 | * a WHERE clause to specify a comparison value. 278 | *
279 | *280 | * However, they are not allowed for identifiers (such as table or column names), 281 | * in the select list that names the columns to be returned by a SELECT statement), 282 | * or to specify both operands of a binary operator such as the = 283 | * equal sign. The latter restriction is necessary because it would be impossible 284 | * to determine the parameter type. In general, parameters are legal only in Data 285 | * Manipulation Language (DML) statements, and not in Data Definition Language 286 | * (DDL) statements. 287 | *
288 | * 289 | * @return bool 290 | *false on error
291 | * 292 | * @since 5.0 293 | */ 294 | public function prepare($query): bool 295 | { 296 | if (!\is_string($query)) { 297 | throw new \InvalidArgumentException('$query was no string: ' . \gettype($query)); 298 | } 299 | 300 | $this->_sql = $query; 301 | $this->_sql_with_bound_parameters = $query; 302 | 303 | if (!$this->_db->isReady()) { 304 | return false; 305 | } 306 | 307 | if (!$query) { 308 | $this->_debug->displayError('Can not prepare an empty query.', false); 309 | 310 | return false; 311 | } 312 | 313 | $bool = parent::prepare($query); 314 | 315 | if ($bool === false) { 316 | $this->_debug->displayError('Can not prepare query: ' . $query . ' | ' . $this->error, true); 317 | } 318 | 319 | return $bool; 320 | } 321 | 322 | /** 323 | * Ger the bound parameters from sql-query as array, if you use the "$this->bind_param_debug()" method. 324 | * 325 | * @return array 326 | */ 327 | public function get_bound_params(): array 328 | { 329 | return $this->_boundParams; 330 | } 331 | 332 | /** 333 | * @return string 334 | */ 335 | public function get_sql(): string 336 | { 337 | return $this->_sql; 338 | } 339 | 340 | /** 341 | * Get the sql-query with bound parameters, if you use the "$this->bind_param_debug()" method. 342 | * 343 | * @return string 344 | */ 345 | public function get_sql_with_bound_parameters(): string 346 | { 347 | return $this->_sql_with_bound_parameters; 348 | } 349 | 350 | /** 351 | * @return int|string 352 | */ 353 | public function insert_id() 354 | { 355 | return $this->insert_id; 356 | } 357 | 358 | /** 359 | * Copies $this->_sql then replaces bound markers with associated values ($this->_sql is not modified 360 | * but the resulting query string is assigned to $this->sql_bound_parameters) 361 | * 362 | * @return string $testQuery - interpolated db query string 363 | */ 364 | private function interpolateQuery(): string 365 | { 366 | $testQuery = $this->_sql; 367 | if ($this->_boundParams) { 368 | /** @noinspection AlterInForeachInspection */ 369 | foreach ($this->_boundParams as &$param) { 370 | $values = $this->_prepareValue($param); 371 | 372 | // set new values 373 | $param['value'] = $values[0]; 374 | // we need to replace the question mark "?" here 375 | $values[1] = \str_replace('?', '###simple_mysqli__prepare_question_mark###', (string)$values[1]); 376 | // build the query (only for debugging) 377 | $testQuery = (string) \preg_replace("/\?/", (string)$values[1], $testQuery, 1); 378 | } 379 | $testQuery = \str_replace('###simple_mysqli__prepare_question_mark###', '?', $testQuery); 380 | } 381 | $this->_sql_with_bound_parameters = $testQuery; 382 | 383 | return $testQuery; 384 | } 385 | 386 | /** 387 | * Error-handling for the sql-query. 388 | * 389 | * @param string $errorMsg 390 | * @param string $sql 391 | * 392 | * @throws DBGoneAwayException 393 | * @throws QueryException 394 | * 395 | * @return bool|int|Result|string "Result" by "SELECT"-queries
222 | *
223 | * INFO: install / use "mysqlnd"-driver for better performance
224 | *
false on error
230 | */ 231 | private function &cast(&$data) 232 | { 233 | if ( 234 | !$this->doctrinePdoStmt // pdo only have limited support for types, so we try to improve it 235 | && 236 | Helper::isMysqlndIsUsed() 237 | ) { 238 | return $data; 239 | } 240 | 241 | // init 242 | static $FIELDS_CACHE = []; 243 | static $TYPES_CACHE = []; 244 | 245 | $result_hash = \spl_object_hash($this->_result); 246 | if (!isset($FIELDS_CACHE[$result_hash])) { 247 | $FIELDS_CACHE[$result_hash] = $this->fetch_fields(); 248 | } 249 | 250 | if ( 251 | !isset($FIELDS_CACHE[$result_hash]) 252 | || 253 | $FIELDS_CACHE[$result_hash] === false 254 | ) { 255 | /** @noinspection PhpUnnecessaryLocalVariableInspection */ 256 | $dataTmp = false; 257 | 258 | return $dataTmp; 259 | } 260 | 261 | if (!isset($TYPES_CACHE[$result_hash])) { 262 | foreach ($FIELDS_CACHE[$result_hash] as $field) { 263 | switch ($field->type) { 264 | case self::MYSQL_TYPE_BIT: 265 | $TYPES_CACHE[$result_hash][$field->name] = 'boolean'; 266 | 267 | break; 268 | case self::MYSQL_TYPE_TINY: 269 | case self::MYSQL_TYPE_SHORT: 270 | case self::MYSQL_TYPE_LONG: 271 | case self::MYSQL_TYPE_LONGLONG: 272 | case self::MYSQL_TYPE_INT24: 273 | $TYPES_CACHE[$result_hash][$field->name] = 'integer'; 274 | 275 | break; 276 | case self::MYSQL_TYPE_DOUBLE: 277 | case self::MYSQL_TYPE_FLOAT: 278 | $TYPES_CACHE[$result_hash][$field->name] = 'float'; 279 | 280 | break; 281 | case self::MYSQL_TYPE_DECIMAL: // INFO: DECIMAL is a "string"-format for numbers 282 | case self::MYSQL_TYPE_NEWDECIMAL: 283 | default: 284 | $TYPES_CACHE[$result_hash][$field->name] = 'string'; 285 | 286 | break; 287 | } 288 | } 289 | } 290 | 291 | if (\is_array($data)) { 292 | foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) { 293 | if (isset($data[$type_name])) { 294 | \settype($data[$type_name], $type); 295 | } 296 | } 297 | } elseif (\is_object($data)) { 298 | foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) { 299 | if (isset($data->{$type_name})) { 300 | \settype($data->{$type_name}, $type); 301 | } 302 | } 303 | } 304 | 305 | return $data; 306 | } 307 | 308 | /** 309 | * Countable interface implementation. 310 | * 311 | * @return int The number of rows in the result 312 | */ 313 | public function count(): int 314 | { 315 | return $this->num_rows; 316 | } 317 | 318 | /** 319 | * Iterator interface implementation. 320 | * 321 | * @return mixed The current element 322 | */ 323 | #[\ReturnTypeWillChange] 324 | public function current() 325 | { 326 | return $this->fetchCallable($this->current_row); 327 | } 328 | 329 | /** 330 | * Iterator interface implementation. 331 | * 332 | * @return int The current element key (row index; zero-based) 333 | */ 334 | public function key(): int 335 | { 336 | return $this->current_row; 337 | } 338 | 339 | /** 340 | * Iterator interface implementation. 341 | * 342 | * @return void 343 | */ 344 | #[\ReturnTypeWillChange] 345 | public function next() 346 | { 347 | $this->current_row++; 348 | } 349 | 350 | /** 351 | * Iterator interface implementation. 352 | * 353 | * @param int $row Row position to rewind to; defaults to 0 354 | * 355 | * @return void 356 | */ 357 | #[\ReturnTypeWillChange] 358 | public function rewind($row = 0) 359 | { 360 | if ($this->seek($row)) { 361 | $this->current_row = $row; 362 | } 363 | } 364 | 365 | /** 366 | * Moves the internal pointer to the specified row position. 367 | * 368 | * @param int $rowRow position; zero-based and set to 0 by default
369 | * 370 | * @return bool 371 | *true on success, false otherwise
372 | */ 373 | #[\ReturnTypeWillChange] 374 | public function seek($row = 0): bool 375 | { 376 | if (\is_int($row) && $row >= 0 && $row < $this->num_rows) { 377 | if ( 378 | $this->doctrineMySQLiStmt 379 | && 380 | $this->doctrineMySQLiStmt instanceof \mysqli_stmt 381 | ) { 382 | $this->doctrineMySQLiStmt->data_seek($row); 383 | 384 | return true; 385 | } 386 | 387 | if ( 388 | $this->doctrinePdoStmt 389 | && 390 | $this->doctrinePdoStmt instanceof \Doctrine\DBAL\Driver\PDOStatement 391 | ) { 392 | return true; 393 | } 394 | 395 | if ($this->_result instanceof \mysqli_result) { 396 | return \mysqli_data_seek($this->_result, $row); 397 | } 398 | } 399 | 400 | return false; 401 | } 402 | 403 | /** 404 | * Iterator interface implementation. 405 | * 406 | * @return bool 407 | *true if the current index is valid, false otherwise
408 | */ 409 | public function valid(): bool 410 | { 411 | return $this->current_row < $this->num_rows; 412 | } 413 | 414 | /** 415 | * Fetch. 416 | * 417 | *
418 | *
419 | * INFO: this will return an object by default, not an array
420 | * and you can change the behaviour via "Result->setDefaultResultType()"
421 | *
Reset the \mysqli_result counter.
424 | * 425 | * @return array|false|object 426 | *false on error
427 | */ 428 | public function &fetch(bool $reset = false) 429 | { 430 | // init 431 | $return = false; 432 | 433 | if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) { 434 | $return = $this->fetchObject(null, null, $reset); 435 | } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) { 436 | $return = $this->fetchArray($reset); 437 | } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) { 438 | $return = $this->fetchArrayy($reset); 439 | } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) { 440 | $return = $this->fetchYield(null, null, $reset); 441 | } 442 | 443 | return $return; 444 | } 445 | 446 | /** 447 | * Fetch all results. 448 | * 449 | *
450 | *
451 | * INFO: this will return an object by default, not an array
452 | * and you can change the behaviour via "Result->setDefaultResultType()"
453 | *
Skip "NULL"-values. | default: false
551 | * 552 | * @return array 553 | *Return an empty array if the "$column" wasn't found
554 | */ 555 | public function fetchAllColumn(string $column, bool $skipNullValues = false): array 556 | { 557 | $return = $this->fetchColumn( 558 | $column, 559 | $skipNullValues, 560 | true 561 | ); 562 | 563 | \assert(\is_array($return)); 564 | 565 | return $return; 566 | } 567 | 568 | /** 569 | * Fetch all results as array with objects. 570 | * 571 | * @param object|string|null $class
572 | * string: create a new object (with optional constructor
573 | * parameter)
574 | * object: use a object and fill the the data into
575 | *
578 | * An array of parameters to pass to the constructor, used if $class is a 579 | * string. 580 | *
581 | * 582 | * @return object[] 583 | * 584 | * @psalm-param class-string|object|null $class 585 | * @psalm-param array
605 | * string: create a new object (with optional constructor
606 | * parameter)
607 | * object: use a object and fill the the data into
608 | *
611 | * An array of parameters to pass to the constructor, used if $class is a 612 | * string. 613 | *
614 | * 615 | * @return \Generator 616 | * @psalm-param class-string|object|null $class 617 | * @psalm-param arrayfalse on error
677 | */ 678 | public function fetchArray(bool $reset = false) 679 | { 680 | if ($reset) { 681 | $this->reset(); 682 | } 683 | 684 | $row = $this->fetch_assoc(); 685 | if ($row) { 686 | $return = $this->cast($row); 687 | 688 | \assert(\is_array($return)); 689 | 690 | return $return; 691 | } 692 | 693 | if ($row === null || $row === false) { 694 | return []; 695 | } 696 | 697 | return false; 698 | } 699 | 700 | /** 701 | * Fetch data as a key/value pair array. 702 | * 703 | *
704 | *
705 | * INFO: both "key" and "value" must exists in the fetched data
706 | * the key will be the new key of the result-array
707 | *
708 | *
712 | * fetchArrayPair('some_id', 'some_value');
713 | * // array(127 => 'some value', 128 => 'some other value')
714 | *
715 | *
716 | * @param string $key
717 | * @param string $value
718 | *
719 | * @return \Arrayy\Arrayy
720 | */
721 | public function fetchArrayPair(string $key, string $value): \Arrayy\Arrayy
722 | {
723 | // init
724 | $arrayPair = new \Arrayy\Arrayy();
725 | $data = $this->fetchAllArrayyYield();
726 |
727 | foreach ($data as $_row) {
728 | assert($_row instanceof \Arrayy\Arrayy);
729 |
730 | if (
731 | $_row->offsetExists($key)
732 | &&
733 | $_row->offsetExists($value)
734 | ) {
735 | $_key = $_row[$key];
736 | $_value = $_row[$value];
737 | $arrayPair[$_key] = $_value;
738 | }
739 | }
740 |
741 | return $arrayPair;
742 | }
743 |
744 | /**
745 | * Fetch as "Arrayy"-object.
746 | *
747 | * @param bool $reset optional Reset the \mysqli_result counter.
748 | * 749 | * @return \Arrayy\Arrayy|false 750 | *false on error
751 | */ 752 | public function fetchArrayy(bool $reset = false) 753 | { 754 | if ($reset) { 755 | $this->reset(); 756 | } 757 | 758 | $row = $this->fetch_assoc(); 759 | if ($row) { 760 | return \Arrayy\Arrayy::create($this->cast($row)); 761 | } 762 | 763 | if ($row === null || $row === false) { 764 | return \Arrayy\Arrayy::create(); 765 | } 766 | 767 | return false; 768 | } 769 | 770 | /** 771 | * Fetches a row or a single column within a row. Returns null if there are 772 | * no more rows in the result. 773 | * 774 | * @param int $row The row number (optional) 775 | * @param string $column The column name (optional) 776 | * 777 | * @return mixed An associative array or a scalar value 778 | */ 779 | public function fetchCallable(int $row = null, string $column = null) 780 | { 781 | if (!$this->num_rows) { 782 | return null; 783 | } 784 | 785 | if ($row !== null) { 786 | $this->seek($row); 787 | } 788 | 789 | $rows = $this->fetch_assoc(); 790 | 791 | if ($column) { 792 | if ( 793 | \is_array($rows) 794 | && 795 | isset($rows[$column]) 796 | ) { 797 | return $rows[$column]; 798 | } 799 | 800 | return null; 801 | } 802 | 803 | if (\is_callable($this->_mapper)) { 804 | return \call_user_func($this->_mapper, $rows); 805 | } 806 | 807 | return $rows; 808 | } 809 | 810 | /** 811 | * Fetch a single column as string (or as 1-dimension array). 812 | * 813 | * @param string $column 814 | * @param bool $skipNullValuesSkip "NULL"-values. | default: true
815 | * @param bool $asArrayGet all values and not only the last one. | default: false
816 | * 817 | * @return array|string 818 | *Return a empty string or an empty array if the "$column" wasn't found, depend on 819 | * "$asArray"
820 | */ 821 | public function &fetchColumn( 822 | string $column = '', 823 | bool $skipNullValues = true, 824 | bool $asArray = false 825 | ) { 826 | if (!$asArray) { 827 | // init 828 | $columnData = ''; 829 | 830 | $data = $this->fetchAllArrayy()->reverse()->getArray(); 831 | foreach ($data as &$_row) { 832 | if ($skipNullValues) { 833 | if (!isset($_row[$column])) { 834 | continue; 835 | } 836 | } elseif (!\array_key_exists($column, $_row)) { 837 | break; 838 | } 839 | 840 | $columnData = $_row[$column]; 841 | 842 | break; 843 | } 844 | 845 | return $columnData; 846 | } 847 | 848 | // -- return as array --> 849 | 850 | // init 851 | $columnData = []; 852 | 853 | foreach ($this->fetchAllYield() as $_row) { 854 | if ($skipNullValues) { 855 | if (!isset($_row->{$column})) { 856 | continue; 857 | } 858 | } elseif (!\property_exists($_row, $column)) { 859 | break; 860 | } 861 | 862 | $columnData[] = $_row->{$column}; 863 | } 864 | 865 | return $columnData; 866 | } 867 | 868 | /** 869 | * Return rows of field information in a result set. 870 | * 871 | * @param bool $as_array Return each field info as array; defaults to false 872 | * 873 | * @return array 874 | *Array of field information each as an associative array.
875 | */ 876 | public function &fetchFields(bool $as_array = false): array 877 | { 878 | $fields = $this->fetch_fields(); 879 | if ($fields === false) { 880 | $fields = []; 881 | 882 | return $fields; 883 | } 884 | 885 | if ($as_array) { 886 | $fields = \array_map( 887 | static function ($object) { 888 | return (array) $object; 889 | }, 890 | $fields 891 | ); 892 | 893 | return $fields; 894 | } 895 | 896 | return $fields; 897 | } 898 | 899 | /** 900 | * Returns all rows at once as a grouped array of scalar values or arrays. 901 | * 902 | * @param string $group The column name to use for grouping 903 | * @param string $column The column name to use as values (optional) 904 | * 905 | * @return array 906 | *A grouped array of scalar values or arrays.
907 | */ 908 | public function &fetchGroups(string $group, string $column = null): array 909 | { 910 | // init 911 | $groups = []; 912 | $pos = $this->current_row; 913 | 914 | foreach ($this->fetchAllArrayyYield() as $row) { 915 | assert($row instanceof \Arrayy\Arrayy); 916 | 917 | if (!$row->offsetExists($group)) { 918 | continue; 919 | } 920 | 921 | if ($column !== null) { 922 | if (!$row->offsetExists($column)) { 923 | continue; 924 | } 925 | 926 | $groups[$row[$group]][] = $row[$column]; 927 | } else { 928 | $groups[$row[$group]][] = $row; 929 | } 930 | } 931 | 932 | $this->rewind($pos); 933 | 934 | return $groups; 935 | } 936 | 937 | /** 938 | * Fetch as object. 939 | * 940 | * @param object|string|null $class
941 | * string: create a new object (with optional constructor
942 | * parameter)
943 | * object: use a object and fill the the data into
944 | *
947 | * An array of parameters to pass to the constructor, used if $class is a 948 | * string. 949 | *
950 | * @param bool $reset optionalReset the \mysqli_result counter.
951 | * 952 | * @return false|object 953 | *false on error
954 | * 955 | * @psalm-param class-string|object|null $class 956 | * @psalm-param arrayAn array of key-value pairs.
1017 | */ 1018 | public function fetchPairs(string $key, string $column = null): array 1019 | { 1020 | // init 1021 | $pairs = []; 1022 | $pos = $this->current_row; 1023 | 1024 | foreach ($this->fetchAllArrayyYield() as $row) { 1025 | assert($row instanceof \Arrayy\Arrayy); 1026 | 1027 | if (!$row->offsetExists($key)) { 1028 | continue; 1029 | } 1030 | 1031 | if ($column !== null) { 1032 | if (!$row->offsetExists($column)) { 1033 | continue; 1034 | } 1035 | 1036 | $pairs[$row[$key]] = $row[$column]; 1037 | } else { 1038 | $pairs[$row[$key]] = $row; 1039 | } 1040 | } 1041 | 1042 | $this->rewind($pos); 1043 | 1044 | return $pairs; 1045 | } 1046 | 1047 | /** 1048 | * Returns all rows at once, transposed as an array of arrays. Instead of 1049 | * returning rows of columns, this method returns columns of rows. 1050 | * 1051 | * @param string $column The column name to use as keys (optional) 1052 | * 1053 | * @return array 1054 | *A transposed array of arrays
1055 | */ 1056 | public function fetchTranspose(string $column = null) 1057 | { 1058 | // init 1059 | $keys = $column !== null ? $this->fetchAllColumn($column) : []; 1060 | $rows = []; 1061 | $pos = $this->current_row; 1062 | 1063 | foreach ($this->fetchAllYield() as $row) { 1064 | foreach ($row as $key => &$value) { 1065 | $rows[$key][] = $value; 1066 | } 1067 | } 1068 | 1069 | $this->rewind($pos); 1070 | 1071 | if (empty($keys)) { 1072 | return $rows; 1073 | } 1074 | 1075 | return \array_map( 1076 | static function ($values) use ($keys) { 1077 | return \array_combine($keys, $values); 1078 | }, 1079 | $rows 1080 | ); 1081 | } 1082 | 1083 | /** 1084 | * Fetch as "\Generator" via yield. 1085 | * 1086 | * @param object|string|null $class
1087 | * string: create a new object (with optional constructor
1088 | * parameter)
1089 | * object: use a object and fill the the data into
1090 | *
1093 | * An array of parameters to pass to the constructor, used if $class is a 1094 | * string. 1095 | *
1096 | * @param bool $reset optionalReset the \mysqli_result counter.
1097 | * 1098 | * @return \Generator 1099 | * 1100 | * @psalm-param class-string|object|null $class 1101 | * @psalm-param arrayA row array or a single scalar value
1254 | */ 1255 | public function first(string $column = null) 1256 | { 1257 | $pos = $this->current_row; 1258 | $first = $this->fetchCallable(0, $column); 1259 | $this->rewind($pos); 1260 | 1261 | return $first; 1262 | } 1263 | 1264 | /** 1265 | * free the memory 1266 | * 1267 | * @return bool 1268 | */ 1269 | public function free() 1270 | { 1271 | if ($this->_result instanceof \mysqli_result) { 1272 | try { 1273 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1274 | @\mysqli_free_result($this->_result); 1275 | } catch (\Throwable $e) { 1276 | return false; 1277 | } 1278 | 1279 | return true; 1280 | } 1281 | 1282 | if ( 1283 | $this->doctrineMySQLiStmt 1284 | && 1285 | $this->doctrineMySQLiStmt instanceof \mysqli_stmt 1286 | ) { 1287 | $this->doctrineMySQLiStmt->free_result(); 1288 | 1289 | return true; 1290 | } 1291 | 1292 | return false; 1293 | } 1294 | 1295 | /** 1296 | * alias for "Result->fetch()" 1297 | * 1298 | * @return array|false|object 1299 | *false on error
1300 | * 1301 | * @see Result::fetch() 1302 | */ 1303 | public function get() 1304 | { 1305 | return $this->fetch(); 1306 | } 1307 | 1308 | /** 1309 | * alias for "Result->fetchAll()" 1310 | * 1311 | * @return array 1312 | * 1313 | * @see Result::fetchAll() 1314 | */ 1315 | public function getAll(): array 1316 | { 1317 | return $this->fetchAll(); 1318 | } 1319 | 1320 | /** 1321 | * alias for "Result->fetchAllColumn()" 1322 | * 1323 | * @param string $column 1324 | * @param bool $skipNullValues 1325 | * 1326 | * @return array 1327 | * 1328 | * @see Result::fetchAllColumn() 1329 | */ 1330 | public function getAllColumn(string $column, bool $skipNullValues = false): array 1331 | { 1332 | return $this->fetchAllColumn($column, $skipNullValues); 1333 | } 1334 | 1335 | /** 1336 | * alias for "Result->fetchAllArray()" 1337 | * 1338 | * @return array 1339 | * 1340 | * @see Result::fetchAllArray() 1341 | */ 1342 | public function getArray(): array 1343 | { 1344 | return $this->fetchAllArray(); 1345 | } 1346 | 1347 | /** 1348 | * alias for "Result->fetchAllArrayy()" 1349 | * 1350 | * @return \Arrayy\Arrayy 1351 | * 1352 | * @see Result::fetchAllArrayy() 1353 | */ 1354 | public function getArrayy(): \Arrayy\Arrayy 1355 | { 1356 | return $this->fetchAllArrayy(); 1357 | } 1358 | 1359 | /** 1360 | * alias for "Result->fetchColumn()" 1361 | * 1362 | * @param string $column 1363 | * @param bool $asArray 1364 | * @param bool $skipNullValues 1365 | * 1366 | * @return array|string 1367 | *Return a empty string or an empty array if the "$column" wasn't found, depend on 1368 | * "$asArray"
1369 | * 1370 | * @see Result::fetchColumn() 1371 | */ 1372 | public function getColumn( 1373 | string $column, 1374 | bool $skipNullValues = true, 1375 | bool $asArray = false 1376 | ) { 1377 | return $this->fetchColumn( 1378 | $column, 1379 | $skipNullValues, 1380 | $asArray 1381 | ); 1382 | } 1383 | 1384 | /** 1385 | * @return string 1386 | */ 1387 | public function getDefaultResultType(): string 1388 | { 1389 | return $this->_default_result_type; 1390 | } 1391 | 1392 | /** 1393 | * alias for "Result->fetchAllObject()" 1394 | * 1395 | * @return array of mysql-objects 1396 | * 1397 | * @see Result::fetchAllObject() 1398 | */ 1399 | public function getObject(): array 1400 | { 1401 | return $this->fetchAllObject(); 1402 | } 1403 | 1404 | /** 1405 | * alias for "Result->fetchAllYield()" 1406 | * 1407 | * @return \Generator 1408 | * 1409 | * @see Result::fetchAllYield() 1410 | */ 1411 | public function getYield(): \Generator 1412 | { 1413 | return $this->fetchAllYield(); 1414 | } 1415 | 1416 | /** 1417 | * Check if the result is empty. 1418 | * 1419 | * @return bool 1420 | */ 1421 | public function is_empty(): bool 1422 | { 1423 | return !($this->num_rows > 0); 1424 | } 1425 | 1426 | /** 1427 | * Fetch all results as "json"-string. 1428 | * 1429 | * @return false|string 1430 | */ 1431 | public function json() 1432 | { 1433 | $data = $this->fetchAllArray(); 1434 | 1435 | return \voku\helper\UTF8::json_encode($data); 1436 | } 1437 | 1438 | /** 1439 | * Returns the last row element from the result. 1440 | * 1441 | * @param string $column The column name to use as value (optional) 1442 | * 1443 | * @return mixed A row array or a single scalar value 1444 | */ 1445 | public function last(string $column = null) 1446 | { 1447 | $pos = $this->current_row; 1448 | $last = $this->fetchCallable($this->num_rows - 1, $column); 1449 | $this->rewind($pos); 1450 | 1451 | return $last; 1452 | } 1453 | 1454 | /** 1455 | * Set the mapper... 1456 | * 1457 | * @param \Closure $callable 1458 | * 1459 | * @return $this 1460 | */ 1461 | public function map(\Closure $callable): self 1462 | { 1463 | $this->_mapper = $callable; 1464 | 1465 | return $this; 1466 | } 1467 | 1468 | /** 1469 | * Alias of count(). Deprecated. 1470 | * 1471 | * @return int 1472 | *The number of rows in the result.
1473 | */ 1474 | public function num_rows(): int 1475 | { 1476 | return $this->count(); 1477 | } 1478 | 1479 | /** 1480 | * ArrayAccess interface implementation. 1481 | * 1482 | * @param int $offsetOffset number
1483 | * 1484 | * @return bool 1485 | *true if offset exists, false otherwise.
1486 | */ 1487 | public function offsetExists($offset): bool 1488 | { 1489 | return \is_int($offset) && $offset >= 0 && $offset < $this->num_rows; 1490 | } 1491 | 1492 | /** 1493 | * ArrayAccess interface implementation. 1494 | * 1495 | * @param int $offset Offset number 1496 | * 1497 | * @return mixed 1498 | */ 1499 | #[\ReturnTypeWillChange] 1500 | public function offsetGet($offset) 1501 | { 1502 | if ($this->offsetExists($offset)) { 1503 | return $this->fetchCallable($offset); 1504 | } 1505 | 1506 | throw new \OutOfBoundsException("undefined offset (${offset})"); 1507 | } 1508 | 1509 | /** 1510 | * ArrayAccess interface implementation. Not implemented by design. 1511 | * 1512 | * @param mixed $offset 1513 | * @param mixed $value 1514 | * 1515 | * @return void 1516 | */ 1517 | #[\ReturnTypeWillChange] 1518 | public function offsetSet($offset, $value) 1519 | { 1520 | } 1521 | 1522 | /** 1523 | * ArrayAccess interface implementation. Not implemented by design. 1524 | * 1525 | * @param mixed $offset 1526 | * 1527 | * @return void 1528 | */ 1529 | #[\ReturnTypeWillChange] 1530 | public function offsetUnset($offset) 1531 | { 1532 | } 1533 | 1534 | /** 1535 | * Reset the offset (data_seek) for the results. 1536 | * 1537 | * @return Result 1538 | */ 1539 | public function reset(): self 1540 | { 1541 | $this->doctrinePdoStmtDataSeekFake = 0; 1542 | 1543 | if (!$this->is_empty()) { 1544 | if ($this->doctrineMySQLiStmt instanceof \mysqli_stmt) { 1545 | $this->doctrineMySQLiStmt->data_seek(0); 1546 | } 1547 | 1548 | if ($this->_result instanceof \mysqli_result) { 1549 | \mysqli_data_seek($this->_result, 0); 1550 | } 1551 | } 1552 | 1553 | return $this; 1554 | } 1555 | 1556 | /** 1557 | * You can set the default result-type to Result::RESULT_TYPE_*. 1558 | * 1559 | * INFO: used for "fetch()" and "fetchAll()" 1560 | * 1561 | * @param string $default_result_type 1562 | * 1563 | * @return void 1564 | */ 1565 | public function setDefaultResultType(string $default_result_type = self::RESULT_TYPE_OBJECT) 1566 | { 1567 | if ( 1568 | $default_result_type === self::RESULT_TYPE_OBJECT 1569 | || 1570 | $default_result_type === self::RESULT_TYPE_ARRAY 1571 | || 1572 | $default_result_type === self::RESULT_TYPE_ARRAYY 1573 | || 1574 | $default_result_type === self::RESULT_TYPE_YIELD 1575 | ) { 1576 | $this->_default_result_type = $default_result_type; 1577 | } 1578 | } 1579 | 1580 | /** 1581 | * @param int $offset 1582 | * @param int|null $length 1583 | * @param bool $preserve_keys 1584 | * 1585 | * @return array 1586 | */ 1587 | public function &slice( 1588 | int $offset = 0, 1589 | int $length = null, 1590 | bool $preserve_keys = false 1591 | ): array { 1592 | // init 1593 | $slice = []; 1594 | 1595 | if ($offset < 0) { 1596 | if (\abs($offset) > $this->num_rows) { 1597 | $offset = 0; 1598 | } else { 1599 | $offset = $this->num_rows - (int) \abs($offset); 1600 | } 1601 | } 1602 | 1603 | $length = $length !== null ? (int) $length : $this->num_rows; 1604 | $n = 0; 1605 | for ($i = $offset; $i < $this->num_rows && $n < $length; $i++) { 1606 | if ($preserve_keys) { 1607 | $slice[$i] = $this->fetchCallable($i); 1608 | } else { 1609 | $slice[] = $this->fetchCallable($i); 1610 | } 1611 | ++$n; 1612 | } 1613 | 1614 | return $slice; 1615 | } 1616 | } 1617 | -------------------------------------------------------------------------------- /src/voku/db/exceptions/DBConnectException.php: -------------------------------------------------------------------------------- 1 |