├── 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 | [![Build Status](https://travis-ci.org/voku/simple-mysqli.svg?branch=master)](https://travis-ci.org/voku/simple-mysqli) 2 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvoku%2Fsimple-mysqli.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvoku%2Fsimple-mysqli?ref=badge_shield) 3 | [![Coverage Status](https://coveralls.io/repos/github/voku/simple-mysqli/badge.svg?branch=master)](https://coveralls.io/github/voku/simple-mysqli?branch=master) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/797ba3ba657d4e0e86f0bade6923fdec)](https://www.codacy.com/app/voku/simple-mysqli) 5 | [![Latest Stable Version](https://poser.pugx.org/voku/simple-mysqli/v/stable)](https://packagist.org/packages/voku/simple-mysqli) 6 | [![Total Downloads](https://poser.pugx.org/voku/simple-mysqli/downloads)](https://packagist.org/packages/voku/simple-mysqli) 7 | [![License](https://poser.pugx.org/voku/simple-mysqli/license)](https://packagist.org/packages/voku/simple-mysqli) 8 | [![Donate to this project using Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/moelleken) 9 | [![Donate to this project using Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](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 | 523 | 524 | e.g.: 525 | 526 | ```php 527 | $sql = " 528 | INSERT INTO foo 529 | SET 530 | page_template = 'lall1', 531 | page_type = 'test1'; 532 | INSERT INTO lall 533 | SET 534 | page_template = 'lall2', 535 | page_type = 'test2'; 536 | INSERT INTO bar 537 | SET 538 | page_template = 'lall3', 539 | page_type = 'test3'; 540 | "; 541 | $result = $this->db->multi_query($sql); // true 542 | 543 | $sql = " 544 | SELECT * FROM foo; 545 | SELECT * FROM lall; 546 | SELECT * FROM bar; 547 | "; 548 | $result = $this->db->multi_query($sql); // Result[] 549 | foreach ($result as $resultForEach) { 550 | $tmpArray = $resultForEach->fetchArray(); 551 | ... 552 | } 553 | ``` 554 | 555 | ## Using the "Prepare"-Class 556 | 557 | Prepare statements have the advantage that they are built together in the MySQL-Server, so the performance is better. 558 | 559 | But the debugging is harder and logging is impossible (via PHP), so we added a wrapper for "bind_param" called "bind_param_debug". 560 | With this wrapper we pre-build the sql-query via php (only for debugging / logging). Now you can e.g. echo the query. 561 | 562 | INFO: You can still use "bind_param" instead of "bind_param_debug", e.g. if you need better performance. 563 | 564 | #### INSERT-Prepare-Query (example) 565 | ```php 566 | use voku\db\DB; 567 | 568 | $db = DB::getInstance(); 569 | 570 | // ------------- 571 | // prepare the queries 572 | 573 | $query = 'INSERT INTO users 574 | SET 575 | name = ?, 576 | email = ? 577 | '; 578 | 579 | $prepare = $db->prepare($query); 580 | 581 | $name = ''; 582 | $email = ''; 583 | 584 | $prepare->bind_param_debug('ss', $name, $email); 585 | 586 | // ------------- 587 | // execute query no. 1 588 | 589 | // INFO: "$template" and "$type" are references, since we use "bind_param" or "bind_param_debug" 590 | $name = 'name_1_中'; 591 | $email = 'foo@bar.com'; 592 | 593 | $prepare->execute(); 594 | 595 | // DEBUG 596 | echo $prepare->get_sql_with_bound_parameters(); 597 | 598 | // ------------- 599 | // execute query no. 2 600 | 601 | // INFO: "$template" and "$type" are references, since we use "bind_param" or "bind_param_debug" 602 | $name = 'Lars'; 603 | $email = 'lars@moelleken.org'; 604 | 605 | $prepare->execute(); 606 | 607 | // DEBUG 608 | echo $prepare->get_sql_with_bound_parameters(); 609 | ``` 610 | 611 | #### SELECT-Prepare-Query (example) 612 | ```php 613 | use voku\db\DB; 614 | 615 | $db = DB::getInstance(); 616 | 617 | // ------------- 618 | // insert some dummy-data, first 619 | 620 | $data = [ 621 | 'page_template' => 'tpl_test_new123123', 622 | 'page_type' => 'ö\'ä"ü', 623 | ]; 624 | 625 | // will return the auto-increment value of the new row 626 | $resultInsert[1] = $db->insert($this->tableName, $data); 627 | $resultInsert[2] = $db->insert($this->tableName, $data); 628 | 629 | // ------------- 630 | // prepare the queries 631 | 632 | $sql = 'SELECT * FROM ' . $this->tableName . ' 633 | WHERE page_id = ? 634 | '; 635 | 636 | $prepare = $this->db->prepare($sql); 637 | $page_id = 0; 638 | $prepare->bind_param_debug('i', $page_id); 639 | 640 | // ------------- 641 | // execute query no. 1 642 | 643 | $page_id = $resultInsert[1]; 644 | $result = $prepare->execute(); 645 | $data = $result->fetchArray(); 646 | 647 | // $data['page_template'] === 'tpl_test_new123123' 648 | // $data['page_id'] === $page_id 649 | 650 | // ------------- 651 | // execute query no. 2 652 | 653 | $page_id = $resultInsert[2]; 654 | $result = $prepare->execute(); 655 | $data = $result->fetchArray(); 656 | 657 | // $data['page_id'] === $page_id 658 | // $data['page_template'] === 'tpl_test_new123123' 659 | ``` 660 | 661 | ### Logging and Errors 662 | 663 | You can hook into the "DB"-Class, so you can use your personal "Logger"-Class. But you have to cover the methods: 664 | 665 | ```php 666 | $this->trace(string $text, string $name) { ... } 667 | $this->debug(string $text, string $name) { ... } 668 | $this->info(string $text, string $name) { ... } 669 | $this->warn(string $text, string $name) { ... } 670 | $this->error(string $text, string $name) { ... } 671 | $this->fatal(string $text, string $name) { ... } 672 | ``` 673 | 674 | You can also disable the logging of every sql-query, with the "getInstance()"-parameter "logger_level" from "DB"-Class. 675 | If you set "logger_level" to something other than "TRACE" or "DEBUG", the "DB"-Class will log only errors anymore. 676 | 677 | ```php 678 | DB::getInstance( 679 | getConfig('db', 'hostname'), // hostname 680 | getConfig('db', 'username'), // username 681 | getConfig('db', 'password'), // password 682 | getConfig('db', 'database'), // database 683 | getConfig('db', 'port'), // port 684 | getConfig('db', 'charset'), // charset 685 | true, // exit_on_error 686 | true, // echo_on_error 687 | 'cms\Logger', // logger_class_name 688 | getConfig('logger', 'level'), // logger_level | 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL' 689 | getConfig('session', 'db') // session_to_db 690 | ); 691 | ``` 692 | 693 | Showing the query log: The log comes with the SQL executed, the execution time and the result row count. 694 | 695 | ```php 696 | print_r($db->log()); 697 | ``` 698 | 699 | To debug mysql errors, use `$db->errors()` to fetch all errors (returns false if there are no errors) or `$db->lastError()` for information about the last error. 700 | 701 | ```php 702 | if ($db->errors()) { 703 | echo $db->lastError(); 704 | } 705 | ``` 706 | 707 | But the easiest way for debugging is to configure "DB"-Class via "DB::getInstance()" to show errors and exit on error (see the example above). Now you can see SQL-errors in your browser if you are working on "localhost" or you can implement your own "checkForDev()" via a simple function, you don't need to extend the "Debug"-Class. If you will receive error-messages via e-mail, you can implement your own "mailToAdmin()"-function instead of extending the "Debug"-Class. 708 | 709 | ### Changelog 710 | 711 | See [CHANGELOG.md](CHANGELOG.md). 712 | 713 | ### Support 714 | 715 | For support and donations please visit [Github](https://github.com/voku/simple-mysqli/) | [Issues](https://github.com/voku/simple-mysqli/issues) | [PayPal](https://paypal.me/moelleken) | [Patreon](https://www.patreon.com/voku). 716 | 717 | For status updates and release announcements please visit [Releases](https://github.com/voku/simple-mysqli/releases) | [Twitter](https://twitter.com/suckup_de) | [Patreon](https://www.patreon.com/voku/posts). 718 | 719 | For professional support please contact [me](https://about.me/voku). 720 | 721 | ### Thanks 722 | 723 | - Thanks to [GitHub](https://github.com) (Microsoft) for hosting the code and a good infrastructure including Issues-Managment, etc. 724 | - Thanks to [IntelliJ](https://www.jetbrains.com) as they make the best IDEs for PHP and they gave me an open source license for PhpStorm! 725 | - Thanks to [Travis CI](https://travis-ci.com/) for being the most awesome, easiest continous integration tool out there! 726 | - Thanks to [StyleCI](https://styleci.io/) for the simple but powerfull code style check. 727 | - Thanks to [PHPStan](https://github.com/phpstan/phpstan) && [Psalm](https://github.com/vimeo/psalm) for relly great Static analysis tools and for discover bugs in the code! 728 | 729 | ### License 730 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvoku%2Fsimple-mysqli.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvoku%2Fsimple-mysqli?ref=badge_large) 731 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voku/simple-mysqli", 3 | "description": "Simple MySQLi library", 4 | "keywords": [ 5 | "php", 6 | "mysqli", 7 | "simple db", 8 | "DB" 9 | ], 10 | "type": "library", 11 | "homepage": "https://github.com/voku/simple-mysqli", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Jonathan Tavares", 16 | "email": "the.entomb@gmail.com", 17 | "homepage": "https://github.com/entomb", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Lars Moelleken", 22 | "email": "lars@moelleken.org", 23 | "homepage": "https://github.com/voku", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=7.0", 29 | "ext-mysqli": "*", 30 | "voku/arrayy": "~6.0 || ~7.0", 31 | "voku/simple-cache": "~4.0", 32 | "voku/portable-utf8": "~6.0", 33 | "voku/phonetic-algorithms": "~5.0", 34 | "symfony/property-access": "~2.8 || ~3.3 || ~4.0 || ~5.0 || ~6.0" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0", 38 | "doctrine/dbal": "~2.0 || ~3.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "voku\\db\\": "src/voku/db/" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "composer/package-versions-deprecated": true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - %currentWorkingDirectory%/src/ 5 | reportUnmatchedIgnoredErrors: false 6 | checkMissingIterableValueType: false 7 | checkGenericClassInNonGenericObjectType: false 8 | ignoreErrors: 9 | # false-positive 10 | - '/call_user_func_array expects callable/' 11 | - '/Argument of an invalid type \(array\)\|object supplied for foreach, only iterables are supported\./' 12 | - '/Argument of an invalid type array\|object supplied for foreach, only iterables are supported\./' 13 | - '/Method voku\\db\\DB::multi_query\(\) should return/' 14 | - '/method mysqli_stmt::__construct\(\) expects/' 15 | - '/Call to an undefined method Doctrine\\DBAL\\Driver\\Statement::getWrappedStatement\(\)\./' 16 | # ignored errors 17 | - '/Result of \&\& is always false/' 18 | - '/Function checkForDev not found/' 19 | - '/Function mailToAdmin not found/' 20 | - '/Method voku\\db\\Result::fetchAll\(\) should return array but returns array\|Arrayy\\Arrayy\|Generator\./' 21 | -------------------------------------------------------------------------------- /src/voku/db/Debug.php: -------------------------------------------------------------------------------- 1 | _db = $db; 94 | } 95 | 96 | /** 97 | * Check is the current user is a developer. 98 | * 99 | * INFO: 100 | * By default we will return "true" if the remote-ip-address is localhost or 101 | * if the script is called via CLI. But you can also overwrite this method or 102 | * you can implement a global "checkForDev()"-function. 103 | * 104 | * @return bool 105 | */ 106 | public function checkForDev(): bool 107 | { 108 | // init 109 | $return = false; 110 | 111 | if (\function_exists('checkForDev')) { 112 | $return = checkForDev(); 113 | } else { 114 | 115 | // for testing with dev-address 116 | $noDev = isset($_GET['noDev']) ? (int) $_GET['noDev'] : 0; 117 | $remoteIpAddress = $_SERVER['REMOTE_ADDR'] ?? false; 118 | 119 | if ( 120 | $noDev !== 1 121 | && 122 | ( 123 | $remoteIpAddress === '127.0.0.1' 124 | || 125 | $remoteIpAddress === '::1' 126 | || 127 | \PHP_SAPI === 'cli' 128 | ) 129 | ) { 130 | $return = true; 131 | } 132 | } 133 | 134 | return $return; 135 | } 136 | 137 | /** 138 | * Clear the errors in "$this->_errors". 139 | * 140 | * @return bool 141 | */ 142 | public function clearErrors(): bool 143 | { 144 | $this->_errors = []; 145 | 146 | return true; 147 | } 148 | 149 | /** 150 | * Display SQL-Errors or throw Exceptions (for dev). 151 | * 152 | * @param string $error

The error message.

153 | * @param bool|null $force_exception_after_error

154 | * 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'] . '
'; 167 | 168 | $this->logger(['error', $log]); 169 | 170 | $this->_errors[] = $log; 171 | 172 | if ( 173 | $this->echo_on_error 174 | && 175 | $this->checkForDev() === true 176 | ) { 177 | $box_border = $this->css_mysql_box_border; 178 | $box_bg = $this->css_mysql_box_bg; 179 | 180 | if (\PHP_SAPI === 'cli') { 181 | echo "\n"; 182 | echo 'Error: ' . $error . "\n"; 183 | echo 'Trace: ' . $fileInfo['path'] . "\n"; 184 | echo "\n"; 185 | } else { 186 | echo ' 187 |
188 | MYSQL Error: 189 | 190 | Error:' . $error . ' 191 |

192 | Trace: ' . $fileInfo['path'] . ' 193 |
194 |
195 | '; 196 | } 197 | } 198 | 199 | if ( 200 | $force_exception_after_error === true 201 | || 202 | ( 203 | $force_exception_after_error === null 204 | && 205 | $this->exit_on_error === true 206 | ) 207 | ) { 208 | throw new QueryException($error); 209 | } 210 | } 211 | 212 | /** 213 | * Get errors from "$this->_errors". 214 | * 215 | * @return array 216 | */ 217 | public function getErrors(): array 218 | { 219 | return $this->_errors; 220 | } 221 | 222 | /** 223 | * Try to get the file & line from the current sql-query. 224 | * 225 | * @return array will return array['path'] 226 | */ 227 | private function getFileAndLineFromSql(): array 228 | { 229 | // init 230 | $return = []; 231 | $path = ''; 232 | $referrer = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); 233 | 234 | foreach ($referrer as $key => $ref) { 235 | if ( 236 | isset($ref['class']) 237 | && 238 | ( 239 | $ref['class'] === DB::class 240 | || 241 | $ref['class'] === self::class 242 | ) 243 | ) { 244 | continue; 245 | } 246 | 247 | $path .= ($referrer[$key]['class'] ?? $referrer[$key]['file'] ?? '') . '::' . ($referrer[$key]['function'] ?? '') . ':' . ($referrer[$key - 1]['line'] ?? '') . ' <- '; 248 | } 249 | 250 | $return['path'] = $path; 251 | 252 | return $return; 253 | } 254 | 255 | /** 256 | * @return string 257 | */ 258 | public function getLoggerClassName(): string 259 | { 260 | return $this->logger_class_name; 261 | } 262 | 263 | /** 264 | * @return string 265 | */ 266 | public function getLoggerLevel(): string 267 | { 268 | return $this->logger_level; 269 | } 270 | 271 | /** 272 | * @return bool 273 | */ 274 | public function isEchoOnError(): bool 275 | { 276 | return $this->echo_on_error; 277 | } 278 | 279 | /** 280 | * @return bool 281 | */ 282 | public function isExitOnError(): bool 283 | { 284 | return $this->exit_on_error; 285 | } 286 | 287 | /** 288 | * Log the current query via "$this->logger". 289 | * 290 | * @param string $sql sql-query 291 | * @param float|int $duration 292 | * @param false|int|string|null $results field_count | insert_id | affected_rows 293 | * @param bool $sql_error 294 | * 295 | * @return false|mixed 296 | *

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",
369 | * the "Logger"-Method is the [0] element from the "$log"-parameter,
370 | * the text you want to log is the [1] element and
371 | * the type you want to log is the next [2] element. 372 | * 373 | * @param string[] $log [method, text, type]
e.g.: array('error', 'this is a error', 'sql') 374 | * 375 | * @return false|mixed 376 | *

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 $dbConnection

Use 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 $dbConnection

Use 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 $language

en, de, fr

168 | * @param string $table 169 | * @param array $whereArray 170 | * @param DB|null $dbConnection

use null if you will use the current database-connection

171 | * @param string|null $databaseName

use 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 $dbConnection

use null if you will use the current database-connection

359 | * @param string|null $databaseName

use 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 $dbConnection

Use null to get your first singleton instance.

420 | * @param string|null $databaseName

use null if you will use the current database

421 | * 422 | * @return false|int|string "int|string" (insert_id) by "INSERT / REPLACE"-queries
423 | * "false" on error 424 | */ 425 | public static function copyTableRow( 426 | string $table, 427 | array $whereArray, 428 | array $updateArray = [], 429 | array $ignoreArray = [], 430 | DB $dbConnection = null, 431 | string $databaseName = null 432 | ) { 433 | // init 434 | $table = \trim($table); 435 | 436 | if ($dbConnection === null) { 437 | $dbConnection = DB::getInstance(); 438 | } 439 | 440 | if ($table === '') { 441 | $debug = new Debug($dbConnection); 442 | $debug->displayError('Invalid table name, table name in empty.', false); 443 | 444 | return false; 445 | } 446 | 447 | $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND'); 448 | if ($whereSQL) { 449 | $whereSQL = 'AND ' . $whereSQL; 450 | } 451 | 452 | if ($databaseName) { 453 | $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.'; 454 | } 455 | 456 | // get the row 457 | $query = 'SELECT * FROM ' . $databaseName . $dbConnection->quote_string($table) . ' 458 | WHERE 1 = 1 459 | ' . $whereSQL . ' 460 | '; 461 | $result = $dbConnection->query($query); 462 | 463 | // make sure the row exists 464 | if ($result instanceof Result && $result->num_rows > 0) { 465 | 466 | /** @noinspection LoopWhichDoesNotLoopInspection */ 467 | /** @noinspection PhpAssignmentInConditionInspection */ 468 | while ($tmpArray = $result->fetchArray()) { 469 | 470 | // re-build a new DB query and ignore some field-names 471 | $bindings = []; 472 | $insert_keys = ''; 473 | $insert_values = ''; 474 | 475 | foreach ($tmpArray as $fieldName => $value) { 476 | if (!\in_array($fieldName, $ignoreArray, true)) { 477 | if (\array_key_exists($fieldName, $updateArray)) { 478 | $insert_keys .= ',' . $fieldName; 479 | $insert_values .= ',?'; 480 | $bindings[] = $updateArray[$fieldName]; // INFO: do not escape non selected data 481 | } else { 482 | $insert_keys .= ',' . $fieldName; 483 | $insert_values .= ',?'; 484 | $bindings[] = $value; // INFO: do not escape non selected data 485 | } 486 | } 487 | } 488 | 489 | $insert_keys = \ltrim($insert_keys, ','); 490 | $insert_values = \ltrim($insert_values, ','); 491 | 492 | // insert the "copied" row 493 | $new_query = 'INSERT INTO ' . $databaseName . $dbConnection->quote_string($table) . ' 494 | (' . $insert_keys . ') 495 | VALUES 496 | (' . $insert_values . ') 497 | '; 498 | 499 | $return = $dbConnection->query($new_query, $bindings); 500 | \assert(\is_int($return) || \is_string($return) || $return === false); 501 | 502 | return $return; 503 | } 504 | } 505 | 506 | return false; 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/voku/db/Prepare.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | private $_boundParams = []; 36 | 37 | /** 38 | * @var DB 39 | */ 40 | private $_db; 41 | 42 | /** 43 | * @var Debug 44 | */ 45 | private $_debug; 46 | 47 | /** 48 | * Prepare constructor. 49 | * 50 | * @param DB $db 51 | * @param string $query 52 | */ 53 | public function __construct(DB $db, string $query) 54 | { 55 | $this->_db = $db; 56 | $this->_debug = $db->getDebugger(); 57 | 58 | if (!$query) { 59 | throw new \InvalidArgumentException('Can not prepare an empty query.'); 60 | } 61 | 62 | parent::__construct($db->getLink(), $query); 63 | 64 | $this->prepare($query); 65 | } 66 | 67 | /** 68 | * Prepare destructor. 69 | */ 70 | public function __destruct() 71 | { 72 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 73 | @$this->close(); 74 | } 75 | 76 | /** 77 | * Combines the values stored in $this->boundParams into one array suitable for pushing as the input arguments to 78 | * parent::bind_param when used with call_user_func_array 79 | * 80 | * @return array{type: string, values: array} 81 | */ 82 | private function _buildArguments(): array 83 | { 84 | $arguments = []; 85 | $arguments['type'] = ''; 86 | 87 | foreach ($this->_boundParams as $param) { 88 | $arguments['type'] .= $param['type']; 89 | $arguments['values'][] = &$param['value']; 90 | } 91 | 92 | return $arguments; 93 | } 94 | 95 | /** 96 | * Escapes the supplied value. 97 | * 98 | * @param array $param 99 | * 100 | * @return array 0 => "$value" escaped
101 | * 1 => "$valueForSqlWithBoundParameters" for insertion into the interpolated query string 102 | */ 103 | private function _prepareValue(array &$param): array 104 | { 105 | $type = $param['type']; // 'i', 'b', 's', 'd' 106 | $value = $param['value']; 107 | 108 | /** @var scalar $value */ 109 | $value = $this->_db->escape($value); 110 | 111 | if ($type === 's') { 112 | $valueForSqlWithBoundParameters = "'" . $value . "'"; 113 | } elseif ($type === 'i') { 114 | $valueForSqlWithBoundParameters = (int) $value; 115 | } elseif ($type === 'd') { 116 | $valueForSqlWithBoundParameters = (float) $value; 117 | } else { 118 | $valueForSqlWithBoundParameters = $value; 119 | } 120 | 121 | return [$value, $valueForSqlWithBoundParameters]; 122 | } 123 | 124 | /** 125 | * @return int|string 126 | */ 127 | public function affected_rows() 128 | { 129 | return $this->affected_rows; 130 | } 131 | 132 | /** 133 | * This is a wrapper for "bind_param" what binds variables to a prepared statement as parameters. If you use this 134 | * wrapper, you can debug your query with e.g. "$this->get_sql_with_bound_parameters()". 135 | * 136 | * @param string $types i corresponding variable has type integer
137 | * d corresponding variable has type double
138 | * s corresponding variable has type string
139 | * b corresponding variable is a blob and will be sent in packets 140 | * 141 | * INFO: We have to explicitly declare all parameters as references, otherwise it does not seem possible to pass 142 | * them on without losing the reference property 143 | * @param mixed ...$args 144 | * 145 | * @return bool 146 | */ 147 | public function bind_param_debug(string $types, &...$args): bool 148 | { 149 | $this->_use_bound_parameters_interpolated = true; 150 | 151 | $typesArray = \str_split($types); 152 | 153 | $args_count = \count($args); 154 | $types_count = \count($typesArray); 155 | 156 | if ($args_count !== $types_count) { 157 | \trigger_error('Number of variables do not match number of parameters in prepared statement', \E_USER_WARNING); 158 | 159 | return false; 160 | } 161 | 162 | $arg = 0; 163 | foreach ($typesArray as $typeInner) { 164 | $val = &$args[$arg]; 165 | $this->_boundParams[] = [ 166 | 'type' => $typeInner, 167 | 'value' => &$val, 168 | ]; 169 | $arg++; 170 | } 171 | 172 | return true; 173 | } 174 | 175 | /** 176 | * {@inheritdoc} 177 | * 178 | * @return bool 179 | */ 180 | public function execute_raw(): bool 181 | { 182 | return parent::execute(); 183 | } 184 | 185 | /** 186 | * Executes a prepared Query 187 | * 188 | * @see http://php.net/manual/en/mysqli-stmt.execute.php 189 | * 190 | * @return bool|int|Result|string "Result" by "SELECT"-queries
191 | * "int|string" (insert_id) by "INSERT / REPLACE"-queries
192 | * "int" (affected_rows) by "UPDATE / DELETE"-queries
193 | * "true" by e.g. "DROP"-queries
194 | * "false" on error 195 | */ 196 | #[\ReturnTypeWillChange] 197 | public function execute() // TODO: php 8.1 use "?array $params = null" here 198 | { 199 | if ($this->_use_bound_parameters_interpolated === true) { 200 | $this->interpolateQuery(); 201 | $args = $this->_buildArguments(); 202 | $this->bind_param($args['type'], ...$args['values']); 203 | } 204 | 205 | $query_start_time = \microtime(true); 206 | $result = parent::execute(); 207 | $query_duration = \microtime(true) - $query_start_time; 208 | 209 | if ($result === true) { 210 | 211 | // "INSERT" || "REPLACE" 212 | if (\preg_match('/^\s*"?(INSERT|REPLACE)\s+/i', $this->_sql)) { 213 | $insert_id = (int) $this->insert_id; 214 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $insert_id); 215 | 216 | return $insert_id; 217 | } 218 | 219 | // "UPDATE" || "DELETE" 220 | if (\preg_match('/^\s*"?(UPDATE|DELETE)\s+/i', $this->_sql)) { 221 | $affected_rows = (int) $this->affected_rows; 222 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $affected_rows); 223 | 224 | return $affected_rows; 225 | } 226 | 227 | // "SELECT" 228 | if (\preg_match('/^\s*"?(SELECT)\s+/i', $this->_sql)) { 229 | $select_result = $this->get_result(); 230 | 231 | if ($select_result === false) { 232 | // log the error query 233 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0, true); 234 | 235 | return $this->queryErrorHandling($this->error, $this->_sql_with_bound_parameters); 236 | } 237 | 238 | $num_rows = (int) $select_result->num_rows; 239 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $num_rows); 240 | 241 | return new Result($this->_sql_with_bound_parameters, $select_result); 242 | } 243 | 244 | // log the ? query 245 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0); 246 | 247 | return true; 248 | } 249 | 250 | // log the error query 251 | $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0, true); 252 | 253 | return $this->queryErrorHandling($this->error, $this->_sql_with_bound_parameters); 254 | } 255 | 256 | /** 257 | * Prepare an SQL statement for execution 258 | * 259 | * @see http://php.net/manual/en/mysqli-stmt.prepare.php 260 | * 261 | * @param string $query

262 | * 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
396 | * "int|string" (insert_id) by "INSERT / REPLACE"-queries
397 | * "int" (affected_rows) by "UPDATE / DELETE"-queries
398 | * "true" by e.g. "DROP"-queries
399 | * "false" on error 400 | */ 401 | private function queryErrorHandling(string $errorMsg, string $sql) 402 | { 403 | if ($errorMsg === 'DB server has gone away' || $errorMsg === 'MySQL server has gone away') { 404 | static $RECONNECT_COUNTER; 405 | 406 | // exit if we have more then 3 "DB server has gone away"-errors 407 | if ($RECONNECT_COUNTER > 3) { 408 | $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMsg . ":\n
" . $sql, 5); 409 | 410 | throw new DBGoneAwayException($errorMsg); 411 | } 412 | 413 | $this->_debug->mailToAdmin('DB-Error', $errorMsg . ":\n
" . $sql); 414 | 415 | // reconnect 416 | $RECONNECT_COUNTER++; 417 | $this->_db->reconnect(true); 418 | 419 | // re-run the current query 420 | return $this->execute(); 421 | } 422 | 423 | $this->_debug->mailToAdmin('SQL-Error', $errorMsg . ":\n
" . $sql); 424 | 425 | // this query returned an error, we must display it (only for dev) !!! 426 | $this->_debug->displayError($errorMsg . ' | ' . $sql); 427 | 428 | return false; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/voku/db/Result.php: -------------------------------------------------------------------------------- 1 | sql = $sql; 141 | 142 | if ( 143 | !$result instanceof \mysqli_result 144 | && 145 | !$result instanceof \Doctrine\DBAL\Driver\Statement 146 | ) { 147 | throw new \InvalidArgumentException('$result must be ' . \mysqli_result::class . ' or ' . \Doctrine\DBAL\Driver\Statement::class . ' !'); 148 | } 149 | 150 | $this->_result = $result; 151 | 152 | if ($this->_result instanceof \Doctrine\DBAL\Driver\Statement) { 153 | if (\method_exists($this->_result, 'getWrappedStatement')) { 154 | throw new \InvalidArgumentException('$result (' . \Doctrine\DBAL\Driver\Statement::class . ') must implement "getWrappedStatement()"!'); 155 | } 156 | 157 | $doctrineDriver = $this->_result->getWrappedStatement(); 158 | 159 | if ($doctrineDriver instanceof \Doctrine\DBAL\Driver\PDOStatement) { 160 | $this->doctrinePdoStmt = $doctrineDriver; 161 | } 162 | 163 | if ($doctrineDriver instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliStatement) { 164 | // try to get the mysqli driver from doctrine 165 | $reflectionTmp = new \ReflectionClass($doctrineDriver); 166 | $propertyTmp = $reflectionTmp->getProperty('_stmt'); 167 | $propertyTmp->setAccessible(true); 168 | $this->doctrineMySQLiStmt = $propertyTmp->getValue($doctrineDriver); 169 | } 170 | 171 | $this->num_rows = $this->_result->rowCount(); 172 | } else { 173 | $this->num_rows = (int) $this->_result->num_rows; 174 | } 175 | 176 | $this->current_row = 0; 177 | 178 | $this->_mapper = $mapper; 179 | } 180 | 181 | /** 182 | * __destruct 183 | */ 184 | public function __destruct() 185 | { 186 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 187 | @$this->free(); 188 | } 189 | 190 | /** 191 | * Runs a user-provided callback with the MySQLi_Result object given as 192 | * argument and returns the result, or returns the MySQLi_Result object if 193 | * called without an argument. 194 | * 195 | * @param callable $callback User-provided callback (optional) 196 | * 197 | * @return \Doctrine\DBAL\Driver\Statement|mixed|\mysqli_result 198 | */ 199 | public function __invoke(callable $callback = null) 200 | { 201 | if ($callback !== null) { 202 | return $callback($this->_result); 203 | } 204 | 205 | return $this->_result; 206 | } 207 | 208 | /** 209 | * Get the current "num_rows" as string. 210 | * 211 | * @return string 212 | */ 213 | public function __toString() 214 | { 215 | return (string) $this->num_rows; 216 | } 217 | 218 | /** 219 | * Cast data into int, float or string. 220 | * 221 | *

222 | *
223 | * INFO: install / use "mysqlnd"-driver for better performance 224 | *

225 | * 226 | * @param array|object $data 227 | * 228 | * @return array|false|object 229 | *

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 $row

Row 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 | *

422 | * 423 | * @param bool $reset optional

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 | *

454 | * 455 | * @return array 456 | */ 457 | public function &fetchAll(): array 458 | { 459 | // init 460 | $return = []; 461 | 462 | if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) { 463 | $return = $this->fetchAllObject(); 464 | } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) { 465 | $return = $this->fetchAllArray(); 466 | } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) { 467 | $return = $this->fetchAllArrayy(); 468 | } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) { 469 | $return = $this->fetchAllYield(); 470 | } 471 | 472 | return $return; 473 | } 474 | 475 | /** 476 | * Fetch all results as array. 477 | * 478 | * @return array 479 | */ 480 | public function &fetchAllArray(): array 481 | { 482 | // init 483 | $data = []; 484 | 485 | if ($this->is_empty()) { 486 | return $data; 487 | } 488 | 489 | $this->reset(); 490 | 491 | /** @noinspection PhpAssignmentInConditionInspection */ 492 | while ($row = $this->fetch_assoc()) { 493 | $data[] = $this->cast($row); 494 | } 495 | 496 | return $data; 497 | } 498 | 499 | /** 500 | * Fetch all results as "Arrayy"-object. 501 | * 502 | * @return \Arrayy\Arrayy 503 | */ 504 | public function &fetchAllArrayy(): \Arrayy\Arrayy 505 | { 506 | // init 507 | $arrayy = \Arrayy\Arrayy::create(); 508 | 509 | if ($this->is_empty()) { 510 | return $arrayy; 511 | } 512 | 513 | $this->reset(); 514 | 515 | /** @noinspection PhpAssignmentInConditionInspection */ 516 | while ($row = $this->fetch_assoc()) { 517 | $arrayy[] = $this->cast($row); 518 | } 519 | 520 | return $arrayy; 521 | } 522 | 523 | /** 524 | * Fetch all results as "Arrayy"-object. 525 | * 526 | * @return \Arrayy\Arrayy<\Arrayy\Arrayy> 527 | */ 528 | public function fetchAllArrayyYield(): \Arrayy\Arrayy 529 | { 530 | if ($this->is_empty()) { 531 | return \Arrayy\Arrayy::create([]); 532 | } 533 | 534 | return \Arrayy\Arrayy::createFromGeneratorFunction( 535 | function () { 536 | $this->reset(); 537 | 538 | /** @noinspection PhpAssignmentInConditionInspection */ 539 | while ($row = $this->fetch_assoc()) { 540 | yield $this->cast($row); 541 | } 542 | } 543 | ); 544 | } 545 | 546 | /** 547 | * Fetch a single column as an 1-dimension array. 548 | * 549 | * @param string $column 550 | * @param bool $skipNullValues

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 | *

576 | * @param array|null $params optional 577 | *

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|null $params 586 | */ 587 | public function &fetchAllObject( 588 | $class = null, 589 | array $params = null 590 | ): array { 591 | // init 592 | $data = []; 593 | 594 | foreach ($this->fetchAllYield($class, $params) as $object) { 595 | $data[] = $object; 596 | } 597 | 598 | return $data; 599 | } 600 | 601 | /** 602 | * Fetch all results as "\Generator" via yield. 603 | * 604 | * @param object|string|null $class

605 | * string: create a new object (with optional constructor 606 | * parameter)
607 | * object: use a object and fill the the data into 608 | *

609 | * @param array|null $params optional 610 | *

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 array|null $params 618 | */ 619 | public function &fetchAllYield( 620 | $class = null, 621 | array $params = null 622 | ): \Generator { 623 | if ($this->is_empty()) { 624 | return; 625 | } 626 | 627 | // init 628 | $this->reset(); 629 | 630 | // fallback 631 | if (!$class || $class === 'stdClass') { 632 | $class = \stdClass::class; 633 | } 634 | 635 | if (\is_object($class)) { 636 | $classTmpOrig = $class; 637 | } elseif ($class && $params) { 638 | $reflectorTmp = new \ReflectionClass($class); 639 | $classTmpOrig = $reflectorTmp->newInstanceArgs($params); 640 | } else { 641 | $classTmpOrig = new $class(); 642 | } 643 | 644 | if ($class === \stdClass::class) { 645 | $propertyAccessor = null; 646 | } else { 647 | $propertyAccessor = \Symfony\Component\PropertyAccess\PropertyAccess::createPropertyAccessor(); 648 | } 649 | 650 | /** @noinspection PhpAssignmentInConditionInspection */ 651 | while ($row = $this->fetch_assoc()) { 652 | $classTmp = clone $classTmpOrig; 653 | 654 | $row = $this->cast($row); 655 | if ($row !== false) { 656 | foreach ($row as $key => $value) { 657 | if ($class === \stdClass::class) { 658 | $classTmp->{$key} = $value; 659 | } else { 660 | \assert($propertyAccessor instanceof \Symfony\Component\PropertyAccess\PropertyAccessor); 661 | $propertyAccessor->setValue($classTmp, $key, $value); 662 | } 663 | } 664 | } 665 | 666 | yield $classTmp; 667 | } 668 | } 669 | 670 | /** 671 | * Fetch as array. 672 | * 673 | * @param bool $reset 674 | * 675 | * @return array|false 676 | *

false 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 | *

709 | * 710 | * e.g.: 711 | * 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 $skipNullValues

Skip "NULL"-values. | default: true

815 | * @param bool $asArray

Get 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 | *

945 | * @param array|null $params optional 946 | *

947 | * An array of parameters to pass to the constructor, used if $class is a 948 | * string. 949 | *

950 | * @param bool $reset optional

Reset 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 array|null $params 957 | */ 958 | public function &fetchObject( 959 | $class = null, 960 | array $params = null, 961 | bool $reset = false 962 | ) { 963 | if ($reset) { 964 | $this->reset(); 965 | } 966 | 967 | // fallback 968 | if (!$class || $class === 'stdClass') { 969 | $class = \stdClass::class; 970 | } 971 | 972 | $row = $this->fetch_assoc(); 973 | $row = $row ? $this->cast($row) : false; 974 | 975 | if (!$row) { 976 | /** @noinspection PhpUnnecessaryLocalVariableInspection */ 977 | $dataTmp = false; 978 | 979 | return $dataTmp; 980 | } 981 | 982 | if (\is_object($class)) { 983 | $classTmp = $class; 984 | } elseif ($class && $params) { 985 | $reflectorTmp = new \ReflectionClass($class); 986 | $classTmp = $reflectorTmp->newInstanceArgs($params); 987 | } else { 988 | $classTmp = new $class(); 989 | } 990 | 991 | if ($class === \stdClass::class) { 992 | $propertyAccessor = null; 993 | } else { 994 | $propertyAccessor = \Symfony\Component\PropertyAccess\PropertyAccess::createPropertyAccessor(); 995 | } 996 | 997 | foreach ($row as $key => &$value) { 998 | if ($class === \stdClass::class) { 999 | $classTmp->{$key} = $value; 1000 | } else { 1001 | \assert($propertyAccessor instanceof \Symfony\Component\PropertyAccess\PropertyAccessor); 1002 | $propertyAccessor->setValue($classTmp, $key, $value); 1003 | } 1004 | } 1005 | 1006 | return $classTmp; 1007 | } 1008 | 1009 | /** 1010 | * Returns all rows at once as key-value pairs. 1011 | * 1012 | * @param string $key The column name to use as keys 1013 | * @param string $column The column name to use as values (optional) 1014 | * 1015 | * @return array 1016 | *

An 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 | *

1091 | * @param array|null $params optional 1092 | *

1093 | * An array of parameters to pass to the constructor, used if $class is a 1094 | * string. 1095 | *

1096 | * @param bool $reset optional

Reset the \mysqli_result counter.

1097 | * 1098 | * @return \Generator 1099 | * 1100 | * @psalm-param class-string|object|null $class 1101 | * @psalm-param array|null $params 1102 | */ 1103 | public function fetchYield( 1104 | $class = null, 1105 | array $params = null, 1106 | bool $reset = false 1107 | ): \Generator { 1108 | if ($reset) { 1109 | $this->reset(); 1110 | } 1111 | 1112 | // fallback 1113 | if (!$class || $class === 'stdClass') { 1114 | $class = \stdClass::class; 1115 | } 1116 | 1117 | if (\is_object($class)) { 1118 | $classTmp = $class; 1119 | } elseif ($class && $params) { 1120 | $reflectorTmp = new \ReflectionClass($class); 1121 | $classTmp = $reflectorTmp->newInstanceArgs($params); 1122 | } else { 1123 | $classTmp = new $class(); 1124 | } 1125 | 1126 | $row = $this->fetch_assoc(); 1127 | $row = $row ? $this->cast($row) : false; 1128 | 1129 | if (!$row) { 1130 | return; 1131 | } 1132 | 1133 | if ($class === \stdClass::class) { 1134 | $propertyAccessor = null; 1135 | } else { 1136 | $propertyAccessor = \Symfony\Component\PropertyAccess\PropertyAccess::createPropertyAccessor(); 1137 | } 1138 | 1139 | /** @noinspection AlterInForeachInspection */ 1140 | foreach ($row as $key => &$value) { 1141 | if ($class === \stdClass::class) { 1142 | $classTmp->{$key} = $value; 1143 | } else { 1144 | \assert($propertyAccessor instanceof \Symfony\Component\PropertyAccess\PropertyAccessor); 1145 | $propertyAccessor->setValue($classTmp, $key, $value); 1146 | } 1147 | } 1148 | 1149 | yield $classTmp; 1150 | } 1151 | 1152 | /** 1153 | * @return mixed|null 1154 | */ 1155 | private function fetch_assoc() 1156 | { 1157 | if ($this->_result instanceof \Doctrine\DBAL\Driver\Statement) { 1158 | if ( 1159 | $this->doctrinePdoStmt 1160 | && 1161 | $this->doctrinePdoStmt instanceof \Doctrine\DBAL\Driver\PDOStatement 1162 | ) { 1163 | if ($this->doctrinePdoStmtDataSeekInit === false) { 1164 | $this->doctrinePdoStmtDataSeekInit = true; 1165 | 1166 | $this->doctrinePdoStmtDataSeekFakeCache = $this->_result->fetchAll(\Doctrine\DBAL\FetchMode::ASSOCIATIVE); 1167 | } 1168 | 1169 | $return = ($this->doctrinePdoStmtDataSeekFakeCache[$this->doctrinePdoStmtDataSeekFake] ?? null); 1170 | 1171 | $this->doctrinePdoStmtDataSeekFake++; 1172 | 1173 | return $return; 1174 | } 1175 | 1176 | if ( 1177 | $this->doctrineMySQLiStmt 1178 | && 1179 | $this->doctrineMySQLiStmt instanceof \mysqli_stmt 1180 | ) { 1181 | return $this->_result->fetch( 1182 | \Doctrine\DBAL\FetchMode::ASSOCIATIVE, 1183 | 0 // FETCH_ORI_NEXT 1184 | ); 1185 | } 1186 | 1187 | return null; 1188 | } 1189 | 1190 | return \mysqli_fetch_assoc($this->_result); 1191 | } 1192 | 1193 | /** 1194 | * @return array|false 1195 | */ 1196 | private function fetch_fields() 1197 | { 1198 | if ($this->_result instanceof \mysqli_result) { 1199 | return \mysqli_fetch_fields($this->_result); 1200 | } 1201 | 1202 | if ($this->doctrineMySQLiStmt) { 1203 | $metadataTmp = $this->doctrineMySQLiStmt->result_metadata(); 1204 | if ($metadataTmp === false) { 1205 | return []; 1206 | } 1207 | 1208 | return $metadataTmp->fetch_fields(); 1209 | } 1210 | 1211 | if ($this->doctrinePdoStmt) { 1212 | $fields = []; 1213 | 1214 | static $THIS_CLASS_TMP = null; 1215 | if ($THIS_CLASS_TMP === null) { 1216 | $THIS_CLASS_TMP = new \ReflectionClass(__CLASS__); 1217 | } 1218 | 1219 | $totalColumnsTmp = $this->doctrinePdoStmt->columnCount(); 1220 | for ($counterTmp = 0; $counterTmp < $totalColumnsTmp; $counterTmp++) { 1221 | $metadataTmp = $this->doctrinePdoStmt->getColumnMeta($counterTmp); 1222 | if ($metadataTmp === false) { 1223 | $metadataTmp = []; 1224 | } 1225 | $fieldTmp = new \stdClass(); 1226 | foreach ($metadataTmp as $metadataTmpKey => $metadataTmpValue) { 1227 | $fieldTmp->{$metadataTmpKey} = $metadataTmpValue; 1228 | } 1229 | 1230 | $typeNativeTmp = 'MYSQL_TYPE_' . ($metadataTmp['native_type'] ?? ''); 1231 | $typeTmp = $THIS_CLASS_TMP->getConstant($typeNativeTmp); 1232 | if ($typeTmp) { 1233 | $fieldTmp->type = $typeTmp; 1234 | } else { 1235 | $fieldTmp->type = ''; 1236 | } 1237 | 1238 | $fields[] = $fieldTmp; 1239 | } 1240 | 1241 | return $fields; 1242 | } 1243 | 1244 | return false; 1245 | } 1246 | 1247 | /** 1248 | * Returns the first row element from the result. 1249 | * 1250 | * @param string $column The column name to use as value (optional) 1251 | * 1252 | * @return mixed 1253 | *

A 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 $offset

Offset 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 |