├── .gitignore ├── .php_cs ├── LICENSE ├── composer.json └── src ├── BaseModel.php ├── BaseRedis.php ├── DB.php ├── PDO.php └── Redis.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | '@Symfony' => true, 16 | '@DoctrineAnnotation' => true, 17 | '@PhpCsFixer' => true, 18 | 'header_comment' => [ 19 | 'commentType' => 'PHPDoc', 20 | 'header' => $header, 21 | 'separate' => 'none', 22 | 'location' => 'after_declare_strict', 23 | ], 24 | 'array_syntax' => [ 25 | 'syntax' => 'short' 26 | ], 27 | 'list_syntax' => [ 28 | 'syntax' => 'short' 29 | ], 30 | 'concat_space' => [ 31 | 'spacing' => 'one' 32 | ], 33 | 'blank_line_before_statement' => [ 34 | 'statements' => [ 35 | 'declare', 36 | ], 37 | ], 38 | 'general_phpdoc_annotation_remove' => [ 39 | 'annotations' => [ 40 | 'author' 41 | ], 42 | ], 43 | 'ordered_imports' => [ 44 | 'imports_order' => [ 45 | 'class', 'function', 'const', 46 | ], 47 | 'sort_algorithm' => 'alpha', 48 | ], 49 | 'single_line_comment_style' => [ 50 | 'comment_types' => [ 51 | ], 52 | ], 53 | 'yoda_style' => [ 54 | 'always_move_variable' => false, 55 | 'equal' => false, 56 | 'identical' => false, 57 | ], 58 | 'phpdoc_align' => [ 59 | 'align' => 'left', 60 | ], 61 | 'multiline_whitespace_before_semicolons' => [ 62 | 'strategy' => 'no_multi_line', 63 | ], 64 | 'constant_case' => [ 65 | 'case' => 'lower', 66 | ], 67 | 'operator_linebreak' => [ 68 | 'only_booleans' => true, 69 | 'position' => 'end', 70 | ], 71 | 'class_attributes_separation' => true, 72 | 'combine_consecutive_unsets' => true, 73 | 'declare_strict_types' => true, 74 | 'linebreak_after_opening_tag' => true, 75 | 'lowercase_static_reference' => true, 76 | 'no_useless_else' => true, 77 | 'no_unused_imports' => true, 78 | 'not_operator_with_successor_space' => true, 79 | 'not_operator_with_space' => false, 80 | 'ordered_class_elements' => true, 81 | 'php_unit_strict' => false, 82 | 'phpdoc_separation' => false, 83 | 'single_quote' => true, 84 | 'standardize_not_equals' => true, 85 | 'multiline_comment_opening_closing' => true, 86 | ]) 87 | ->setFinder( 88 | PhpCsFixer\Finder::create() 89 | ->exclude('public') 90 | ->exclude('runtime') 91 | ->exclude('vendor') 92 | ->in(__DIR__) 93 | ) 94 | ->setUsingCache(false); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-swoole/db", 3 | "description": "A db component for Simps.", 4 | "type": "library", 5 | "license": "Apache-2.0", 6 | "keywords": [ 7 | "php", 8 | "swoole", 9 | "coroutine", 10 | "simple", 11 | "simps", 12 | "db" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Luffy", 17 | "email": "52o@qq52o.cn" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=7.1", 22 | "ext-swoole": ">=4.4" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^2.9" 26 | }, 27 | "suggest": { 28 | "swoole/library": "Required to use database, and the swoole version is less than 4.4.17" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Simps\\DB\\": "src/" 33 | } 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "scripts": { 39 | "cs-fix": "php-cs-fixer fix $1" 40 | } 41 | } -------------------------------------------------------------------------------- /src/BaseModel.php: -------------------------------------------------------------------------------- 1 | pool = \Simps\DB\PDO::getInstance($config); 60 | } else { 61 | $this->pool = \Simps\DB\PDO::getInstance(); 62 | } 63 | } 64 | 65 | public function beginTransaction() 66 | { 67 | if ($this->in_transaction) { //嵌套事务 68 | throw new RuntimeException('do not support nested transaction now'); 69 | } 70 | $this->realGetConn(); 71 | $this->pdo->beginTransaction(); 72 | $this->in_transaction = true; 73 | Coroutine::defer(function () { 74 | if ($this->in_transaction) { 75 | $this->rollBack(); 76 | } 77 | }); 78 | } 79 | 80 | public function commit(): void 81 | { 82 | $this->pdo->commit(); 83 | $this->in_transaction = false; 84 | $this->release($this->pdo); 85 | } 86 | 87 | public function rollBack(): void 88 | { 89 | $this->pdo->rollBack(); 90 | $this->in_transaction = false; 91 | $this->release($this->pdo); 92 | } 93 | 94 | public function action($actions) 95 | { 96 | if (is_callable($actions)) { 97 | $this->beginTransaction(); 98 | 99 | try { 100 | $result = $actions($this); 101 | 102 | if ($result === false) { 103 | $this->rollBack(); 104 | } else { 105 | $this->commit(); 106 | } 107 | } catch (Exception $e) { 108 | $this->rollBack(); 109 | 110 | throw $e; 111 | } 112 | 113 | return $result; 114 | } 115 | 116 | return false; 117 | } 118 | 119 | public function query($query, $map = []) 120 | { 121 | $raw = $this->raw($query, $map); 122 | 123 | $query = $this->buildRaw($raw, $map); 124 | 125 | return $this->exec($query, $map); 126 | } 127 | 128 | public function exec($query, $map = []) 129 | { 130 | $this->realGetConn(); 131 | 132 | $this->statement = null; 133 | 134 | if ($this->debug_mode) { 135 | echo $this->generate($query, $map); 136 | 137 | $this->debug_mode = false; 138 | 139 | $this->release($this->pdo); 140 | 141 | return false; 142 | } 143 | 144 | if ($this->logging) { 145 | $this->logs[] = [$query, $map]; 146 | } else { 147 | $this->logs = [[$query, $map]]; 148 | } 149 | $statement = $this->pdo->prepare($query); 150 | 151 | if (! $statement) { 152 | $this->errorInfo = $this->pdo->errorInfo(); 153 | $this->statement = null; 154 | 155 | $this->release($this->pdo); 156 | 157 | return false; 158 | } 159 | 160 | $this->statement = $statement; 161 | 162 | foreach ($map as $key => $value) { 163 | $statement->bindValue($key, $value[0], $value[1]); 164 | } 165 | 166 | $execute = $statement->execute(); 167 | 168 | $this->errorInfo = $statement->errorInfo(); 169 | 170 | if (! $execute) { 171 | $this->statement = null; 172 | } 173 | 174 | $lastId = $this->pdo->lastInsertId(); 175 | 176 | if ($lastId != '0' && $lastId != '') { 177 | $this->release($this->pdo); 178 | 179 | return $lastId; 180 | } 181 | 182 | $this->release($this->pdo); 183 | 184 | return $statement; 185 | } 186 | 187 | public static function raw($string, $map = []) 188 | { 189 | $raw = new Raw(); 190 | 191 | $raw->map = $map; 192 | $raw->value = $string; 193 | 194 | return $raw; 195 | } 196 | 197 | public function quote($string) 198 | { 199 | $this->realGetConn(); 200 | $ret = $this->pdo->quote($string); 201 | $this->release($this->pdo); 202 | return $ret; 203 | } 204 | 205 | public function create($table, $columns, $options = null) 206 | { 207 | $stack = []; 208 | 209 | $tableName = $table; 210 | 211 | foreach ($columns as $name => $definition) { 212 | if (is_int($name)) { 213 | $stack[] = preg_replace('/\<([a-zA-Z0-9_]+)\>/i', '"$1"', $definition); 214 | } elseif (is_array($definition)) { 215 | $stack[] = $name . ' ' . implode(' ', $definition); 216 | } elseif (is_string($definition)) { 217 | $stack[] = $name . ' ' . $this->query($definition); 218 | } 219 | } 220 | 221 | $table_option = ''; 222 | 223 | if (is_array($options)) { 224 | $option_stack = []; 225 | 226 | foreach ($options as $key => $value) { 227 | if (is_string($value) || is_int($value)) { 228 | $option_stack[] = "{$key} = {$value}"; 229 | } 230 | } 231 | 232 | $table_option = ' ' . implode(', ', $option_stack); 233 | } elseif (is_string($options)) { 234 | $table_option = ' ' . $options; 235 | } 236 | 237 | return $this->exec("CREATE TABLE IF NOT EXISTS {$tableName} (" . implode(', ', $stack) . "){$table_option}"); 238 | } 239 | 240 | public function drop($table) 241 | { 242 | $tableName = $table; 243 | 244 | return $this->exec("DROP TABLE IF EXISTS {$tableName}"); 245 | } 246 | 247 | public function select($table, $join, $columns = null, $where = null) 248 | { 249 | $map = []; 250 | $result = []; 251 | $column_map = []; 252 | 253 | $index = 0; 254 | 255 | $column = $where === null ? $join : $columns; 256 | 257 | $is_single = (is_string($column) && $column !== '*'); 258 | 259 | $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map); 260 | 261 | $this->columnMap($columns, $column_map, true); 262 | 263 | if (! $this->statement) { 264 | return false; 265 | } 266 | 267 | if ($columns === '*') { 268 | return $query->fetchAll(PDO::FETCH_ASSOC); 269 | } 270 | 271 | while ($data = $query->fetch(PDO::FETCH_ASSOC)) { 272 | $current_stack = []; 273 | $this->dataMap($data, $columns, $column_map, $current_stack, true, $result); 274 | } 275 | 276 | if ($is_single) { 277 | $single_result = []; 278 | $result_key = $column_map[$column][0]; 279 | 280 | foreach ($result as $item) { 281 | $single_result[] = $item[$result_key]; 282 | } 283 | 284 | return $single_result; 285 | } 286 | 287 | return $result; 288 | } 289 | 290 | public function insert($table, $datas) 291 | { 292 | $stack = []; 293 | $columns = []; 294 | $fields = []; 295 | $map = []; 296 | 297 | if (! isset($datas[0])) { 298 | $datas = [$datas]; 299 | } 300 | 301 | foreach ($datas as $data) { 302 | foreach ($data as $key => $value) { 303 | $columns[] = $key; 304 | } 305 | } 306 | 307 | $columns = array_unique($columns); 308 | 309 | foreach ($datas as $data) { 310 | $values = []; 311 | 312 | foreach ($columns as $key) { 313 | if ($raw = $this->buildRaw($data[$key], $map)) { 314 | $values[] = $raw; 315 | continue; 316 | } 317 | 318 | $map_key = $this->mapKey(); 319 | 320 | $values[] = $map_key; 321 | 322 | if (! isset($data[$key])) { 323 | $map[$map_key] = [null, PDO::PARAM_NULL]; 324 | } else { 325 | $value = $data[$key]; 326 | 327 | $type = gettype($value); 328 | 329 | switch ($type) { 330 | case 'array': 331 | $map[$map_key] = [ 332 | strpos($key, '[JSON]') === strlen($key) - 6 ? 333 | json_encode($value) : 334 | serialize($value), 335 | PDO::PARAM_STR, 336 | ]; 337 | break; 338 | case 'object': 339 | $value = serialize($value); 340 | 341 | // no break 342 | case 'NULL': 343 | case 'resource': 344 | case 'boolean': 345 | case 'integer': 346 | case 'double': 347 | case 'string': 348 | $map[$map_key] = $this->typeMap($value, $type); 349 | break; 350 | } 351 | } 352 | } 353 | 354 | $stack[] = '(' . implode(', ', $values) . ')'; 355 | } 356 | 357 | foreach ($columns as $key) { 358 | $fields[] = $this->columnQuote(preg_replace('/(\\s*\\[JSON\\]$)/i', '', $key)); 359 | } 360 | 361 | return $this->exec( 362 | 'INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode( 363 | ', ', 364 | $stack 365 | ), 366 | $map 367 | ); 368 | } 369 | 370 | public function update($table, $data, $where = null) 371 | { 372 | $fields = []; 373 | $map = []; 374 | 375 | foreach ($data as $key => $value) { 376 | $column = $this->columnQuote(preg_replace('/(\\s*\\[(JSON|\\+|\\-|\\*|\\/)\\]$)/i', '', $key)); 377 | 378 | if ($raw = $this->buildRaw($value, $map)) { 379 | $fields[] = $column . ' = ' . $raw; 380 | continue; 381 | } 382 | 383 | $map_key = $this->mapKey(); 384 | 385 | preg_match('/(?[a-zA-Z0-9_]+)(\[(?\+|\-|\*|\/)\])?/i', $key, $match); 386 | 387 | if (isset($match['operator'])) { 388 | if (is_numeric($value)) { 389 | $fields[] = $column . ' = ' . $column . ' ' . $match['operator'] . ' ' . $value; 390 | } 391 | } else { 392 | $fields[] = $column . ' = ' . $map_key; 393 | 394 | $type = gettype($value); 395 | 396 | switch ($type) { 397 | case 'array': 398 | $map[$map_key] = [ 399 | strpos($key, '[JSON]') === strlen($key) - 6 ? 400 | json_encode($value) : 401 | serialize($value), 402 | PDO::PARAM_STR, 403 | ]; 404 | break; 405 | case 'object': 406 | $value = serialize($value); 407 | 408 | // no break 409 | case 'NULL': 410 | case 'resource': 411 | case 'boolean': 412 | case 'integer': 413 | case 'double': 414 | case 'string': 415 | $map[$map_key] = $this->typeMap($value, $type); 416 | break; 417 | } 418 | } 419 | } 420 | 421 | return $this->exec( 422 | 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $fields) . $this->whereClause($where, $map), 423 | $map 424 | ); 425 | } 426 | 427 | public function delete($table, $where) 428 | { 429 | $map = []; 430 | 431 | return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where, $map), $map); 432 | } 433 | 434 | public function replace($table, $columns, $where = null) 435 | { 436 | if (! is_array($columns) || empty($columns)) { 437 | return false; 438 | } 439 | 440 | $map = []; 441 | $stack = []; 442 | 443 | foreach ($columns as $column => $replacements) { 444 | if (is_array($replacements)) { 445 | foreach ($replacements as $old => $new) { 446 | $map_key = $this->mapKey(); 447 | 448 | $stack[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote( 449 | $column 450 | ) . ', ' . $map_key . 'a, ' . $map_key . 'b)'; 451 | 452 | $map[$map_key . 'a'] = [$old, PDO::PARAM_STR]; 453 | $map[$map_key . 'b'] = [$new, PDO::PARAM_STR]; 454 | } 455 | } 456 | } 457 | 458 | if (! empty($stack)) { 459 | return $this->exec( 460 | 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause( 461 | $where, 462 | $map 463 | ), 464 | $map 465 | ); 466 | } 467 | 468 | return false; 469 | } 470 | 471 | public function get($table, $join = null, $columns = null, $where = null) 472 | { 473 | $map = []; 474 | $result = []; 475 | $column_map = []; 476 | $current_stack = []; 477 | 478 | if ($where === null) { 479 | $column = $join; 480 | unset($columns['LIMIT']); 481 | } else { 482 | $column = $columns; 483 | unset($where['LIMIT']); 484 | } 485 | 486 | $is_single = (is_string($column) && $column !== '*'); 487 | 488 | $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where) . ' LIMIT 1', $map); 489 | 490 | if (! $this->statement) { 491 | return false; 492 | } 493 | 494 | $data = $query->fetchAll(PDO::FETCH_ASSOC); 495 | 496 | if (isset($data[0])) { 497 | if ($column === '*') { 498 | return $data[0]; 499 | } 500 | 501 | $this->columnMap($columns, $column_map, true); 502 | 503 | $this->dataMap($data[0], $columns, $column_map, $current_stack, true, $result); 504 | 505 | if ($is_single) { 506 | return $result[0][$column_map[$column][0]]; 507 | } 508 | 509 | return $result[0]; 510 | } 511 | } 512 | 513 | public function has($table, $join, $where = null) 514 | { 515 | $map = []; 516 | $column = null; 517 | 518 | $query = $this->exec( 519 | 'SELECT EXISTS(' . $this->selectContext($table, $map, $join, $column, $where, 1) . ')', 520 | $map 521 | ); 522 | 523 | if (! $this->statement) { 524 | return false; 525 | } 526 | 527 | $result = $query->fetchColumn(); 528 | 529 | return $result === '1' || $result === 1 || $result === true; 530 | } 531 | 532 | public function rand($table, $join = null, $columns = null, $where = null) 533 | { 534 | $order = 'RANDOM()'; 535 | 536 | $order_raw = $this->raw($order); 537 | 538 | if ($where === null) { 539 | if ($columns === null) { 540 | $columns = [ 541 | 'ORDER' => $order_raw, 542 | ]; 543 | } else { 544 | $column = $join; 545 | unset($columns['ORDER']); 546 | 547 | $columns['ORDER'] = $order_raw; 548 | } 549 | } else { 550 | unset($where['ORDER']); 551 | 552 | $where['ORDER'] = $order_raw; 553 | } 554 | 555 | return $this->select($table, $join, $columns, $where); 556 | } 557 | 558 | public function count($table, $join = null, $column = null, $where = null) 559 | { 560 | return $this->aggregate('count', $table, $join, $column, $where); 561 | } 562 | 563 | public function avg($table, $join, $column = null, $where = null) 564 | { 565 | return $this->aggregate('avg', $table, $join, $column, $where); 566 | } 567 | 568 | public function max($table, $join, $column = null, $where = null) 569 | { 570 | return $this->aggregate('max', $table, $join, $column, $where); 571 | } 572 | 573 | public function min($table, $join, $column = null, $where = null) 574 | { 575 | return $this->aggregate('min', $table, $join, $column, $where); 576 | } 577 | 578 | public function sum($table, $join, $column = null, $where = null) 579 | { 580 | return $this->aggregate('sum', $table, $join, $column, $where); 581 | } 582 | 583 | public function debug() 584 | { 585 | $this->debug_mode = true; 586 | 587 | return $this; 588 | } 589 | 590 | public function error() 591 | { 592 | return $this->errorInfo; 593 | } 594 | 595 | public function last() 596 | { 597 | $log = end($this->logs); 598 | 599 | return $this->generate($log[0], $log[1]); 600 | } 601 | 602 | public function log() 603 | { 604 | return array_map( 605 | function ($log) { 606 | return $this->generate($log[0], $log[1]); 607 | }, 608 | $this->logs 609 | ); 610 | } 611 | 612 | public function info() 613 | { 614 | $output = [ 615 | 'server' => 'SERVER_INFO', 616 | 'driver' => 'DRIVER_NAME', 617 | 'client' => 'CLIENT_VERSION', 618 | 'version' => 'SERVER_VERSION', 619 | 'connection' => 'CONNECTION_STATUS', 620 | ]; 621 | 622 | foreach ($output as $key => $value) { 623 | $output[$key] = @$this->pdo->getAttribute(constant('PDO::ATTR_' . $value)); 624 | } 625 | return $output; 626 | } 627 | 628 | public function release($connection = null) 629 | { 630 | if ($connection === null) { 631 | $this->in_transaction = false; 632 | } 633 | 634 | if (! $this->in_transaction) { 635 | $this->pool->close($connection); 636 | return true; 637 | } 638 | 639 | return false; 640 | } 641 | 642 | protected function generate($query, $map) 643 | { 644 | $query = preg_replace( 645 | '/"([a-zA-Z0-9_]+)"/i', 646 | '`$1`', 647 | $query 648 | ); 649 | 650 | foreach ($map as $key => $value) { 651 | if ($value[1] === PDO::PARAM_STR) { 652 | $replace = $this->quote($value[0]); 653 | } elseif ($value[1] === PDO::PARAM_NULL) { 654 | $replace = 'NULL'; 655 | } elseif ($value[1] === PDO::PARAM_LOB) { 656 | $replace = '{LOB_DATA}'; 657 | } else { 658 | $replace = $value[0]; 659 | } 660 | 661 | $query = str_replace($key, $replace, $query); 662 | } 663 | 664 | return $query; 665 | } 666 | 667 | protected function isRaw($object) 668 | { 669 | return $object instanceof Raw; 670 | } 671 | 672 | protected function buildRaw($raw, &$map) 673 | { 674 | if (! $this->isRaw($raw)) { 675 | return false; 676 | } 677 | 678 | $query = preg_replace_callback( 679 | '/(([`\']).*?)?((FROM|TABLE|INTO|UPDATE|JOIN)\s*)?\<(([a-zA-Z0-9_]+)(\.[a-zA-Z0-9_]+)?)\>(.*?\2)?/i', 680 | function ($matches) { 681 | if (! empty($matches[2]) && isset($matches[8])) { 682 | return $matches[0]; 683 | } 684 | 685 | if (! empty($matches[4])) { 686 | return $matches[1] . $matches[4] . ' ' . $this->tableQuote($matches[5]); 687 | } 688 | 689 | return $matches[1] . $this->columnQuote($matches[5]); 690 | }, 691 | $raw->value 692 | ); 693 | 694 | $raw_map = $raw->map; 695 | 696 | if (! empty($raw_map)) { 697 | foreach ($raw_map as $key => $value) { 698 | $map[$key] = $this->typeMap($value, gettype($value)); 699 | } 700 | } 701 | 702 | return $query; 703 | } 704 | 705 | protected function tableQuote($table) 706 | { 707 | if (! preg_match('/^[a-zA-Z0-9_]+$/i', $table)) { 708 | throw new InvalidArgumentException("Incorrect table name \"{$table}\""); 709 | } 710 | 711 | return '"' . $table . '"'; 712 | } 713 | 714 | protected function mapKey() 715 | { 716 | return ':MeDoO_' . $this->guid++ . '_mEdOo'; 717 | } 718 | 719 | protected function typeMap($value, $type) 720 | { 721 | $map = [ 722 | 'NULL' => PDO::PARAM_NULL, 723 | 'integer' => PDO::PARAM_INT, 724 | 'double' => PDO::PARAM_STR, 725 | 'boolean' => PDO::PARAM_BOOL, 726 | 'string' => PDO::PARAM_STR, 727 | 'object' => PDO::PARAM_STR, 728 | 'resource' => PDO::PARAM_LOB, 729 | ]; 730 | 731 | if ($type === 'boolean') { 732 | $value = ($value ? '1' : '0'); 733 | } elseif ($type === 'NULL') { 734 | $value = null; 735 | } 736 | 737 | return [$value, $map[$type]]; 738 | } 739 | 740 | protected function columnQuote($string) 741 | { 742 | if (! preg_match('/^[a-zA-Z0-9_]+(\.?[a-zA-Z0-9_]+)?$/i', $string)) { 743 | throw new InvalidArgumentException("Incorrect column name \"{$string}\""); 744 | } 745 | 746 | if (strpos($string, '.') !== false) { 747 | return '"' . str_replace('.', '"."', $string) . '"'; 748 | } 749 | 750 | return '"' . $string . '"'; 751 | } 752 | 753 | protected function columnPush(&$columns, &$map, $root, $is_join = false) 754 | { 755 | if ($columns === '*') { 756 | return $columns; 757 | } 758 | 759 | $stack = []; 760 | 761 | if (is_string($columns)) { 762 | $columns = [$columns]; 763 | } 764 | 765 | foreach ($columns as $key => $value) { 766 | if (! is_int($key) && is_array($value) && $root && count(array_keys($columns)) === 1) { 767 | $stack[] = $this->columnQuote($key); 768 | 769 | $stack[] = $this->columnPush($value, $map, false, $is_join); 770 | } elseif (is_array($value)) { 771 | $stack[] = $this->columnPush($value, $map, false, $is_join); 772 | } elseif (! is_int($key) && $raw = $this->buildRaw($value, $map)) { 773 | preg_match('/(?[a-zA-Z0-9_\.]+)(\s*\[(?(String|Bool|Int|Number))\])?/i', $key, $match); 774 | 775 | $stack[] = $raw . ' AS ' . $this->columnQuote($match['column']); 776 | } elseif (is_int($key) && is_string($value)) { 777 | if ($is_join && strpos($value, '*') !== false) { 778 | throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table'); 779 | } 780 | 781 | preg_match( 782 | '/(?[a-zA-Z0-9_\.]+)(?:\s*\((?[a-zA-Z0-9_]+)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/i', 783 | $value, 784 | $match 785 | ); 786 | 787 | if (! empty($match['alias'])) { 788 | $stack[] = $this->columnQuote($match['column']) . ' AS ' . $this->columnQuote($match['alias']); 789 | 790 | $columns[$key] = $match['alias']; 791 | 792 | if (! empty($match['type'])) { 793 | $columns[$key] .= ' [' . $match['type'] . ']'; 794 | } 795 | } else { 796 | $stack[] = $this->columnQuote($match['column']); 797 | } 798 | } 799 | } 800 | 801 | return implode(',', $stack); 802 | } 803 | 804 | protected function arrayQuote($array) 805 | { 806 | $stack = []; 807 | 808 | foreach ($array as $value) { 809 | $stack[] = is_int($value) ? $value : $this->pdo->quote($value); 810 | } 811 | 812 | return implode(',', $stack); 813 | } 814 | 815 | protected function innerConjunct($data, $map, $conjunctor, $outer_conjunctor) 816 | { 817 | $stack = []; 818 | 819 | foreach ($data as $value) { 820 | $stack[] = '(' . $this->dataImplode($value, $map, $conjunctor) . ')'; 821 | } 822 | 823 | return implode($outer_conjunctor . ' ', $stack); 824 | } 825 | 826 | protected function dataImplode($data, &$map, $conjunctor) 827 | { 828 | $stack = []; 829 | 830 | foreach ($data as $key => $value) { 831 | $type = gettype($value); 832 | 833 | if ( 834 | $type === 'array' && 835 | preg_match('/^(AND|OR)(\\s+#.*)?$/', $key, $relation_match) 836 | ) { 837 | $relationship = $relation_match[1]; 838 | 839 | $stack[] = $value !== array_keys(array_keys($value)) ? 840 | '(' . $this->dataImplode($value, $map, ' ' . $relationship) . ')' : 841 | '(' . $this->innerConjunct($value, $map, ' ' . $relationship, $conjunctor) . ')'; 842 | 843 | continue; 844 | } 845 | 846 | $map_key = $this->mapKey(); 847 | 848 | if ( 849 | is_int($key) && 850 | preg_match('/([a-zA-Z0-9_\.]+)\[(?\>\=?|\<\=?|\!?\=)\]([a-zA-Z0-9_\.]+)/i', $value, $match) 851 | ) { 852 | $stack[] = $this->columnQuote($match[1]) . ' ' . $match['operator'] . ' ' . $this->columnQuote( 853 | $match[3] 854 | ); 855 | } else { 856 | preg_match( 857 | '/([a-zA-Z0-9_\.]+)(\[(?\>\=?|\<\=?|\!|\<\>|\>\<|\!?~|REGEXP)\])?/i', 858 | $key, 859 | $match 860 | ); 861 | $column = $this->columnQuote($match[1]); 862 | 863 | if (isset($match['operator'])) { 864 | $operator = $match['operator']; 865 | 866 | if (in_array($operator, ['>', '>=', '<', '<='])) { 867 | $condition = $column . ' ' . $operator . ' '; 868 | 869 | if (is_numeric($value)) { 870 | $condition .= $map_key; 871 | $map[$map_key] = [$value, is_float($value) ? PDO::PARAM_STR : PDO::PARAM_INT]; 872 | } elseif ($raw = $this->buildRaw($value, $map)) { 873 | $condition .= $raw; 874 | } else { 875 | $condition .= $map_key; 876 | $map[$map_key] = [$value, PDO::PARAM_STR]; 877 | } 878 | 879 | $stack[] = $condition; 880 | } elseif ($operator === '!') { 881 | switch ($type) { 882 | case 'NULL': 883 | $stack[] = $column . ' IS NOT NULL'; 884 | break; 885 | case 'array': 886 | $placeholders = []; 887 | 888 | foreach ($value as $index => $item) { 889 | $stack_key = $map_key . $index . '_i'; 890 | 891 | $placeholders[] = $stack_key; 892 | $map[$stack_key] = $this->typeMap($item, gettype($item)); 893 | } 894 | 895 | $stack[] = $column . ' NOT IN (' . implode(', ', $placeholders) . ')'; 896 | break; 897 | case 'object': 898 | if ($raw = $this->buildRaw($value, $map)) { 899 | $stack[] = $column . ' != ' . $raw; 900 | } 901 | break; 902 | case 'integer': 903 | case 'double': 904 | case 'boolean': 905 | case 'string': 906 | $stack[] = $column . ' != ' . $map_key; 907 | $map[$map_key] = $this->typeMap($value, $type); 908 | break; 909 | } 910 | } elseif ($operator === '~' || $operator === '!~') { 911 | if ($type !== 'array') { 912 | $value = [$value]; 913 | } 914 | 915 | $connector = ' OR '; 916 | $data = array_values($value); 917 | 918 | if (is_array($data[0])) { 919 | if (isset($value['AND']) || isset($value['OR'])) { 920 | $connector = ' ' . array_keys($value)[0] . ' '; 921 | $value = $data[0]; 922 | } 923 | } 924 | 925 | $like_clauses = []; 926 | 927 | foreach ($value as $index => $item) { 928 | $item = strval($item); 929 | 930 | if (! preg_match('/(\[.+\]|[\*\?\!\%#^-_]|%.+|.+%)/', $item)) { 931 | $item = '%' . $item . '%'; 932 | } 933 | 934 | $like_clauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $map_key . 'L' . $index; 935 | $map[$map_key . 'L' . $index] = [$item, PDO::PARAM_STR]; 936 | } 937 | 938 | $stack[] = '(' . implode($connector, $like_clauses) . ')'; 939 | } elseif ($operator === '<>' || $operator === '><') { 940 | if ($type === 'array') { 941 | if ($operator === '><') { 942 | $column .= ' NOT'; 943 | } 944 | 945 | $stack[] = '(' . $column . ' BETWEEN ' . $map_key . 'a AND ' . $map_key . 'b)'; 946 | 947 | $data_type = (is_numeric($value[0]) && is_numeric( 948 | $value[1] 949 | )) ? PDO::PARAM_INT : PDO::PARAM_STR; 950 | 951 | $map[$map_key . 'a'] = [$value[0], $data_type]; 952 | $map[$map_key . 'b'] = [$value[1], $data_type]; 953 | } 954 | } elseif ($operator === 'REGEXP') { 955 | $stack[] = $column . ' REGEXP ' . $map_key; 956 | $map[$map_key] = [$value, PDO::PARAM_STR]; 957 | } 958 | } else { 959 | switch ($type) { 960 | case 'NULL': 961 | $stack[] = $column . ' IS NULL'; 962 | break; 963 | case 'array': 964 | $placeholders = []; 965 | 966 | foreach ($value as $index => $item) { 967 | $stack_key = $map_key . $index . '_i'; 968 | 969 | $placeholders[] = $stack_key; 970 | $map[$stack_key] = $this->typeMap($item, gettype($item)); 971 | } 972 | 973 | $stack[] = $column . ' IN (' . implode(', ', $placeholders) . ')'; 974 | break; 975 | case 'object': 976 | if ($raw = $this->buildRaw($value, $map)) { 977 | $stack[] = $column . ' = ' . $raw; 978 | } 979 | break; 980 | case 'integer': 981 | case 'double': 982 | case 'boolean': 983 | case 'string': 984 | $stack[] = $column . ' = ' . $map_key; 985 | $map[$map_key] = $this->typeMap($value, $type); 986 | break; 987 | } 988 | } 989 | } 990 | } 991 | 992 | return implode($conjunctor . ' ', $stack); 993 | } 994 | 995 | protected function whereClause($where, &$map) 996 | { 997 | $where_clause = ''; 998 | 999 | if (is_array($where)) { 1000 | $where_keys = array_keys($where); 1001 | 1002 | $conditions = array_diff_key( 1003 | $where, 1004 | array_flip( 1005 | ['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH'] 1006 | ) 1007 | ); 1008 | 1009 | if (! empty($conditions)) { 1010 | $where_clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND'); 1011 | } 1012 | 1013 | if (isset($where['GROUP'])) { 1014 | $GROUP = $where['GROUP']; 1015 | 1016 | if (is_array($GROUP)) { 1017 | $stack = []; 1018 | 1019 | foreach ($GROUP as $column => $value) { 1020 | $stack[] = $this->columnQuote($value); 1021 | } 1022 | 1023 | $where_clause .= ' GROUP BY ' . implode(',', $stack); 1024 | } elseif ($raw = $this->buildRaw($GROUP, $map)) { 1025 | $where_clause .= ' GROUP BY ' . $raw; 1026 | } else { 1027 | $where_clause .= ' GROUP BY ' . $this->columnQuote($GROUP); 1028 | } 1029 | 1030 | if (isset($where['HAVING'])) { 1031 | if ($raw = $this->buildRaw($where['HAVING'], $map)) { 1032 | $where_clause .= ' HAVING ' . $raw; 1033 | } else { 1034 | $where_clause .= ' HAVING ' . $this->dataImplode($where['HAVING'], $map, ' AND'); 1035 | } 1036 | } 1037 | } 1038 | 1039 | if (isset($where['ORDER'])) { 1040 | $ORDER = $where['ORDER']; 1041 | 1042 | if (is_array($ORDER)) { 1043 | $stack = []; 1044 | 1045 | foreach ($ORDER as $column => $value) { 1046 | if (is_array($value)) { 1047 | $stack[] = 'FIELD(' . $this->columnQuote($column) . ', ' . $this->arrayQuote($value) . ')'; 1048 | } elseif ($value === 'ASC' || $value === 'DESC') { 1049 | $stack[] = $this->columnQuote($column) . ' ' . $value; 1050 | } elseif (is_int($column)) { 1051 | $stack[] = $this->columnQuote($value); 1052 | } 1053 | } 1054 | 1055 | $where_clause .= ' ORDER BY ' . implode(',', $stack); 1056 | } elseif ($raw = $this->buildRaw($ORDER, $map)) { 1057 | $where_clause .= ' ORDER BY ' . $raw; 1058 | } else { 1059 | $where_clause .= ' ORDER BY ' . $this->columnQuote($ORDER); 1060 | } 1061 | } 1062 | 1063 | if (isset($where['LIMIT'])) { 1064 | $LIMIT = $where['LIMIT']; 1065 | 1066 | if (is_numeric($LIMIT)) { 1067 | $where_clause .= ' LIMIT ' . $LIMIT; 1068 | } elseif ( 1069 | is_array($LIMIT) && 1070 | is_numeric($LIMIT[0]) && 1071 | is_numeric($LIMIT[1]) 1072 | ) { 1073 | $where_clause .= ' LIMIT ' . $LIMIT[1] . ' OFFSET ' . $LIMIT[0]; 1074 | } 1075 | } 1076 | } elseif ($raw = $this->buildRaw($where, $map)) { 1077 | $where_clause .= ' ' . $raw; 1078 | } 1079 | 1080 | return $where_clause; 1081 | } 1082 | 1083 | protected function selectContext($table, &$map, $join, &$columns = null, $where = null, $column_fn = null) 1084 | { 1085 | preg_match('/(?[a-zA-Z0-9_]+)\s*\((?[a-zA-Z0-9_]+)\)/i', $table, $table_match); 1086 | 1087 | if (isset($table_match['table'], $table_match['alias'])) { 1088 | $table = $this->tableQuote($table_match['table']); 1089 | 1090 | $table_query = $table . ' AS ' . $this->tableQuote($table_match['alias']); 1091 | } else { 1092 | $table = $this->tableQuote($table); 1093 | 1094 | $table_query = $table; 1095 | } 1096 | 1097 | $is_join = false; 1098 | $join_key = is_array($join) ? array_keys($join) : null; 1099 | 1100 | if ( 1101 | isset($join_key[0]) && 1102 | strpos((string) $join_key[0], '[') === 0 1103 | ) { 1104 | $is_join = true; 1105 | $table_query .= ' ' . $this->buildJoin($table, $join); 1106 | } else { 1107 | if (is_null($columns)) { 1108 | if ( 1109 | ! is_null($where) || 1110 | (is_array($join) && isset($column_fn)) 1111 | ) { 1112 | $where = $join; 1113 | $columns = null; 1114 | } else { 1115 | $where = null; 1116 | $columns = $join; 1117 | } 1118 | } else { 1119 | $where = $columns; 1120 | $columns = $join; 1121 | } 1122 | } 1123 | 1124 | if (isset($column_fn)) { 1125 | if ($column_fn === 1) { 1126 | $column = '1'; 1127 | 1128 | if (is_null($where)) { 1129 | $where = $columns; 1130 | } 1131 | } elseif ($raw = $this->buildRaw($column_fn, $map)) { 1132 | $column = $raw; 1133 | } else { 1134 | if (empty($columns) || $this->isRaw($columns)) { 1135 | $columns = '*'; 1136 | $where = $join; 1137 | } 1138 | 1139 | $column = $column_fn . '(' . $this->columnPush($columns, $map, true) . ')'; 1140 | } 1141 | } else { 1142 | $column = $this->columnPush($columns, $map, true, $is_join); 1143 | } 1144 | 1145 | return 'SELECT ' . $column . ' FROM ' . $table_query . $this->whereClause($where, $map); 1146 | } 1147 | 1148 | protected function buildJoin($table, $join) 1149 | { 1150 | $table_join = []; 1151 | 1152 | $join_array = [ 1153 | '>' => 'LEFT', 1154 | '<' => 'RIGHT', 1155 | '<>' => 'FULL', 1156 | '><' => 'INNER', 1157 | ]; 1158 | 1159 | foreach ($join as $sub_table => $relation) { 1160 | preg_match( 1161 | '/(\[(?\<\>?|\>\[a-zA-Z0-9_]+)\s?(\((?[a-zA-Z0-9_]+)\))?/', 1162 | $sub_table, 1163 | $match 1164 | ); 1165 | 1166 | if ($match['join'] !== '' && $match['table'] !== '') { 1167 | if (is_string($relation)) { 1168 | $relation = 'USING ("' . $relation . '")'; 1169 | } 1170 | 1171 | if (is_array($relation)) { 1172 | // For ['column1', 'column2'] 1173 | if (isset($relation[0])) { 1174 | $relation = 'USING ("' . implode('", "', $relation) . '")'; 1175 | } else { 1176 | $joins = []; 1177 | 1178 | foreach ($relation as $key => $value) { 1179 | $joins[] = ( 1180 | strpos($key, '.') > 0 ? 1181 | // For ['tableB.column' => 'column'] 1182 | $this->columnQuote($key) : 1183 | 1184 | // For ['column1' => 'column2'] 1185 | $table . '."' . $key . '"' 1186 | ) . 1187 | ' = ' . 1188 | $this->tableQuote( 1189 | isset($match['alias']) ? $match['alias'] : $match['table'] 1190 | ) . '."' . $value . '"'; 1191 | } 1192 | 1193 | $relation = 'ON ' . implode(' AND ', $joins); 1194 | } 1195 | } 1196 | 1197 | $table_name = $this->tableQuote($match['table']) . ' '; 1198 | 1199 | if (isset($match['alias'])) { 1200 | $table_name .= 'AS ' . $this->tableQuote($match['alias']) . ' '; 1201 | } 1202 | 1203 | $table_join[] = $join_array[$match['join']] . ' JOIN ' . $table_name . $relation; 1204 | } 1205 | } 1206 | 1207 | return implode(' ', $table_join); 1208 | } 1209 | 1210 | protected function columnMap($columns, &$stack, $root) 1211 | { 1212 | if ($columns === '*') { 1213 | return $stack; 1214 | } 1215 | 1216 | foreach ($columns as $key => $value) { 1217 | if (is_int($key)) { 1218 | preg_match( 1219 | '/([a-zA-Z0-9_]+\.)?(?[a-zA-Z0-9_]+)(?:\s*\((?[a-zA-Z0-9_]+)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/i', 1220 | $value, 1221 | $key_match 1222 | ); 1223 | 1224 | $column_key = ! empty($key_match['alias']) ? 1225 | $key_match['alias'] : 1226 | $key_match['column']; 1227 | 1228 | if (isset($key_match['type'])) { 1229 | $stack[$value] = [$column_key, $key_match['type']]; 1230 | } else { 1231 | $stack[$value] = [$column_key, 'String']; 1232 | } 1233 | } elseif ($this->isRaw($value)) { 1234 | preg_match( 1235 | '/([a-zA-Z0-9_]+\.)?(?[a-zA-Z0-9_]+)(\s*\[(?(String|Bool|Int|Number))\])?/i', 1236 | $key, 1237 | $key_match 1238 | ); 1239 | 1240 | $column_key = $key_match['column']; 1241 | 1242 | if (isset($key_match['type'])) { 1243 | $stack[$key] = [$column_key, $key_match['type']]; 1244 | } else { 1245 | $stack[$key] = [$column_key, 'String']; 1246 | } 1247 | } elseif (! is_int($key) && is_array($value)) { 1248 | if ($root && count(array_keys($columns)) === 1) { 1249 | $stack[$key] = [$key, 'String']; 1250 | } 1251 | 1252 | $this->columnMap($value, $stack, false); 1253 | } 1254 | } 1255 | 1256 | return $stack; 1257 | } 1258 | 1259 | protected function dataMap($data, $columns, $column_map, &$stack, $root, &$result) 1260 | { 1261 | if ($root) { 1262 | $columns_key = array_keys($columns); 1263 | 1264 | if (count($columns_key) === 1 && is_array($columns[$columns_key[0]])) { 1265 | $index_key = array_keys($columns)[0]; 1266 | $data_key = preg_replace('/^[a-zA-Z0-9_]+\\./i', '', $index_key); 1267 | 1268 | $current_stack = []; 1269 | 1270 | foreach ($data as $item) { 1271 | $this->dataMap($data, $columns[$index_key], $column_map, $current_stack, false, $result); 1272 | $index = $data[$data_key]; 1273 | 1274 | $result[$index] = $current_stack; 1275 | } 1276 | } else { 1277 | $current_stack = []; 1278 | 1279 | $this->dataMap($data, $columns, $column_map, $current_stack, false, $result); 1280 | 1281 | $result[] = $current_stack; 1282 | } 1283 | 1284 | return; 1285 | } 1286 | 1287 | foreach ($columns as $key => $value) { 1288 | $isRaw = $this->isRaw($value); 1289 | 1290 | if (is_int($key) || $isRaw) { 1291 | $map = $column_map[$isRaw ? $key : $value]; 1292 | 1293 | $column_key = $map[0]; 1294 | 1295 | $item = $data[$column_key]; 1296 | 1297 | if (isset($map[1])) { 1298 | if ($isRaw && in_array($map[1], ['Object', 'JSON'])) { 1299 | continue; 1300 | } 1301 | 1302 | if (is_null($item)) { 1303 | $stack[$column_key] = null; 1304 | continue; 1305 | } 1306 | 1307 | switch ($map[1]) { 1308 | case 'Number': 1309 | $stack[$column_key] = (float) $item; 1310 | break; 1311 | case 'Int': 1312 | $stack[$column_key] = (int) $item; 1313 | break; 1314 | case 'Bool': 1315 | $stack[$column_key] = (bool) $item; 1316 | break; 1317 | case 'Object': 1318 | $stack[$column_key] = unserialize($item); 1319 | break; 1320 | case 'JSON': 1321 | $stack[$column_key] = json_decode($item, true); 1322 | break; 1323 | case 'String': 1324 | $stack[$column_key] = $item; 1325 | break; 1326 | } 1327 | } else { 1328 | $stack[$column_key] = $item; 1329 | } 1330 | } else { 1331 | $current_stack = []; 1332 | 1333 | $this->dataMap($data, $value, $column_map, $current_stack, false, $result); 1334 | 1335 | $stack[$key] = $current_stack; 1336 | } 1337 | } 1338 | } 1339 | 1340 | private function realGetConn() 1341 | { 1342 | if (! $this->in_transaction) { 1343 | $this->pdo = $this->pool->getConnection(); 1344 | $this->pdo->exec('SET SQL_MODE=ANSI_QUOTES'); 1345 | } 1346 | } 1347 | 1348 | private function aggregate($type, $table, $join = null, $column = null, $where = null) 1349 | { 1350 | $map = []; 1351 | 1352 | $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, strtoupper($type)), $map); 1353 | 1354 | if (! $this->statement) { 1355 | return false; 1356 | } 1357 | 1358 | $number = $query->fetchColumn(); 1359 | 1360 | return is_numeric($number) ? $number + 0 : $number; 1361 | } 1362 | } 1363 | -------------------------------------------------------------------------------- /src/BaseRedis.php: -------------------------------------------------------------------------------- 1 | pool = Redis::getInstance($config, $poolName); 26 | } 27 | 28 | public function __call($name, $arguments) 29 | { 30 | if (! $this->multiOnGoing) { 31 | $this->connection = $this->pool->getConnection(); 32 | } 33 | 34 | try { 35 | $data = $this->connection->{$name}(...$arguments); 36 | } catch (\RedisException $e) { 37 | $this->pool->close(null); 38 | throw $e; 39 | } 40 | 41 | if ($this->multiOnGoing) { 42 | return $this; 43 | } 44 | $this->pool->close($this->connection); 45 | 46 | return $data; 47 | } 48 | 49 | public function brPop($keys, $timeout) 50 | { 51 | $this->connection = $this->pool->getConnection(); 52 | 53 | $data = []; 54 | 55 | try { 56 | $start = time(); 57 | $data = $this->connection->brPop($keys, $timeout); 58 | } catch (\RedisException $e) { 59 | $end = time(); 60 | if ($end - $start < $timeout) { 61 | $this->pool->close(null); 62 | throw $e; 63 | } 64 | } 65 | 66 | $this->pool->close($this->connection); 67 | 68 | return $data; 69 | } 70 | 71 | public function blPop($keys, $timeout) 72 | { 73 | $this->connection = $this->pool->getConnection(); 74 | 75 | $data = []; 76 | 77 | try { 78 | $start = time(); 79 | $data = $this->connection->blPop($keys, $timeout); 80 | } catch (\RedisException $e) { 81 | $end = time(); 82 | if ($end - $start < $timeout) { 83 | $this->pool->close(null); 84 | throw $e; 85 | } 86 | } 87 | 88 | $this->pool->close($this->connection); 89 | 90 | return $data; 91 | } 92 | 93 | public function subscribe($channels, $callback) 94 | { 95 | $this->connection = $this->pool->getConnection(); 96 | 97 | $this->connection->setOption(\Redis::OPT_READ_TIMEOUT, '-1'); 98 | 99 | try { 100 | $data = $this->connection->subscribe($channels, $callback); 101 | } catch (\RedisException $e) { 102 | $this->pool->close(null); 103 | throw $e; 104 | } 105 | 106 | $this->connection->setOption(\Redis::OPT_READ_TIMEOUT, (string) $this->pool->getConfig()['time_out']); 107 | 108 | $this->pool->close($this->connection); 109 | 110 | return $data; 111 | } 112 | 113 | public function brpoplpush($srcKey, $dstKey, $timeout) 114 | { 115 | $this->connection = $this->pool->getConnection(); 116 | 117 | try { 118 | $start = time(); 119 | $data = $this->connection->brpoplpush($srcKey, $dstKey, $timeout); 120 | } catch (\RedisException $e) { 121 | $end = time(); 122 | if ($end - $start < $timeout) { 123 | $this->pool->close(null); 124 | throw $e; 125 | } 126 | $data = false; 127 | } 128 | 129 | $this->pool->close($this->connection); 130 | 131 | return $data; 132 | } 133 | 134 | public function fill() 135 | { 136 | $this->pool->fill(); 137 | } 138 | 139 | public function watch($key) 140 | { 141 | if (! $this->multiOnGoing) { 142 | $this->connection = $this->pool->getConnection(); 143 | 144 | try { 145 | $this->connection->watch($key); 146 | } catch (\RedisException $e) { 147 | $this->pool->close(null); 148 | throw $e; 149 | } 150 | 151 | $this->isWatching = true; 152 | } 153 | 154 | return $this; 155 | } 156 | 157 | public function unwatch() 158 | { 159 | if (! $this->isWatching) { 160 | return true; 161 | } 162 | 163 | try { 164 | $result = $this->connection->unwatch(); 165 | } catch (\RedisException $e) { 166 | $this->isWatching = false; 167 | $this->pool->close(null); 168 | throw $e; 169 | } 170 | 171 | $this->isWatching = false; 172 | 173 | $this->pool->close($this->connection); 174 | 175 | return $result; 176 | } 177 | 178 | public function multi($mode = \Redis::MULTI) 179 | { 180 | if (! $this->multiOnGoing) { 181 | if (! $this->isWatching) { 182 | $this->connection = $this->pool->getConnection(); 183 | } 184 | 185 | try { 186 | $this->connection->multi($mode); 187 | } catch (\RedisException $e) { 188 | $this->pool->close(null); 189 | throw $e; 190 | } 191 | 192 | $this->multiOnGoing = true; 193 | } 194 | 195 | return $this; 196 | } 197 | 198 | public function exec() 199 | { 200 | if (! $this->multiOnGoing) { 201 | return; 202 | } 203 | 204 | try { 205 | $result = $this->connection->exec(); 206 | } catch (\RedisException $e) { 207 | $this->isWatching = false; 208 | $this->multiOnGoing = false; 209 | $this->pool->close(null); 210 | throw $e; 211 | } 212 | 213 | $this->isWatching = false; 214 | $this->multiOnGoing = false; 215 | 216 | $this->pool->close($this->connection); 217 | 218 | return $result; 219 | } 220 | 221 | public function discard() 222 | { 223 | if (! $this->multiOnGoing) { 224 | return; 225 | } 226 | 227 | try { 228 | $result = $this->connection->discard(); 229 | } catch (\RedisException $e) { 230 | $this->isWatching = false; 231 | $this->multiOnGoing = false; 232 | $this->pool->close(null); 233 | throw $e; 234 | } 235 | 236 | $this->isWatching = false; 237 | $this->multiOnGoing = false; 238 | 239 | $this->pool->close($this->connection); 240 | 241 | return $result; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/DB.php: -------------------------------------------------------------------------------- 1 | pool = \Simps\DB\PDO::getInstance($config); 33 | } else { 34 | $this->pool = \Simps\DB\PDO::getInstance(); 35 | } 36 | } 37 | 38 | public function quote(string $string, int $parameter_type = PDO::PARAM_STR) 39 | { 40 | $this->realGetConn(); 41 | try { 42 | $ret = $this->pdo->quote($string, $parameter_type); 43 | } catch (Throwable $th) { 44 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 45 | $this->release(); 46 | } else { 47 | $this->release($this->pdo); 48 | } 49 | throw $th; 50 | } 51 | 52 | $this->release($this->pdo); 53 | return $ret; 54 | } 55 | 56 | public function beginTransaction(): void 57 | { 58 | if ($this->in_transaction) { //嵌套事务 59 | throw new RuntimeException('do not support nested transaction now'); 60 | } 61 | $this->realGetConn(); 62 | try { 63 | $this->pdo->beginTransaction(); 64 | } catch (Throwable $th) { 65 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 66 | $this->release(); 67 | } else { 68 | $this->release($this->pdo); 69 | } 70 | throw $th; 71 | } 72 | $this->in_transaction = true; 73 | Coroutine::defer(function () { 74 | if ($this->in_transaction) { 75 | $this->rollBack(); 76 | } 77 | }); 78 | } 79 | 80 | public function commit(): void 81 | { 82 | $this->in_transaction = false; 83 | try { 84 | $this->pdo->commit(); 85 | } catch (Throwable $th) { 86 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 87 | $this->release(); 88 | } else { 89 | $this->release($this->pdo); 90 | } 91 | throw $th; 92 | } 93 | $this->release($this->pdo); 94 | } 95 | 96 | public function rollBack(): void 97 | { 98 | $this->in_transaction = false; 99 | 100 | try { 101 | $this->pdo->rollBack(); 102 | } catch (Throwable $th) { 103 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 104 | $this->release(); 105 | } else { 106 | $this->release($this->pdo); 107 | } 108 | throw $th; 109 | } 110 | 111 | $this->release($this->pdo); 112 | } 113 | 114 | public function query(string $query, array $bindings = []): array 115 | { 116 | $this->realGetConn(); 117 | try { 118 | $statement = $this->pdo->prepare($query); 119 | 120 | $this->bindValues($statement, $bindings); 121 | 122 | $statement->execute(); 123 | 124 | $ret = $statement->fetchAll(); 125 | } catch (Throwable $th) { 126 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 127 | $this->release(); 128 | } else { 129 | $this->release($this->pdo); 130 | } 131 | throw $th; 132 | } 133 | 134 | $this->release($this->pdo); 135 | 136 | return $ret; 137 | } 138 | 139 | public function fetch(string $query, array $bindings = []) 140 | { 141 | $records = $this->query($query, $bindings); 142 | 143 | return array_shift($records); 144 | } 145 | 146 | public function execute(string $query, array $bindings = []): int 147 | { 148 | $this->realGetConn(); 149 | try { 150 | $statement = $this->pdo->prepare($query); 151 | 152 | $this->bindValues($statement, $bindings); 153 | 154 | $statement->execute(); 155 | 156 | $ret = $statement->rowCount(); 157 | } catch (Throwable $th) { 158 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 159 | $this->release(); 160 | } else { 161 | $this->release($this->pdo); 162 | } 163 | throw $th; 164 | } 165 | 166 | $this->release($this->pdo); 167 | 168 | return $ret; 169 | } 170 | 171 | public function exec(string $sql): int 172 | { 173 | $this->realGetConn(); 174 | try { 175 | $ret = $this->pdo->exec($sql); 176 | } catch (Throwable $th) { 177 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 178 | $this->release(); 179 | } else { 180 | $this->release($this->pdo); 181 | } 182 | throw $th; 183 | } 184 | 185 | $this->release($this->pdo); 186 | 187 | return $ret; 188 | } 189 | 190 | public function insert(string $query, array $bindings = []): int 191 | { 192 | $this->realGetConn(); 193 | 194 | try { 195 | $statement = $this->pdo->prepare($query); 196 | 197 | $this->bindValues($statement, $bindings); 198 | 199 | $statement->execute(); 200 | 201 | $ret = (int) $this->pdo->lastInsertId(); 202 | } catch (Throwable $th) { 203 | if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) { 204 | $this->release(); 205 | } else { 206 | $this->release($this->pdo); 207 | } 208 | throw $th; 209 | } 210 | 211 | $this->release($this->pdo); 212 | 213 | return $ret; 214 | } 215 | 216 | public function release($connection = null) 217 | { 218 | if ($connection === null) { 219 | $this->in_transaction = false; 220 | } 221 | 222 | if (! $this->in_transaction) { 223 | $this->pool->close($connection); 224 | return true; 225 | } 226 | 227 | return false; 228 | } 229 | 230 | protected function bindValues(PDOStatementProxy $statement, array $bindings): void 231 | { 232 | foreach ($bindings as $key => $value) { 233 | $statement->bindValue( 234 | is_string($key) ? $key : $key + 1, 235 | $value, 236 | is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR 237 | ); 238 | } 239 | } 240 | 241 | private function realGetConn() 242 | { 243 | if (! $this->in_transaction) { 244 | $this->pdo = $this->pool->getConnection(); 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/PDO.php: -------------------------------------------------------------------------------- 1 | 'localhost', 26 | 'port' => 3306, 27 | 'database' => 'test', 28 | 'username' => 'root', 29 | 'password' => 'root', 30 | 'charset' => 'utf8mb4', 31 | 'unixSocket' => null, 32 | 'options' => [], 33 | 'size' => 64, 34 | ]; 35 | 36 | private static $instance; 37 | 38 | private function __construct(array $config) 39 | { 40 | if (empty($this->pools)) { 41 | $this->config = array_replace_recursive($this->config, $config); 42 | $this->pools = new PDOPool( 43 | (new PDOConfig()) 44 | ->withHost($this->config['host']) 45 | ->withPort($this->config['port']) 46 | ->withUnixSocket($this->config['unixSocket']) 47 | ->withDbName($this->config['database']) 48 | ->withCharset($this->config['charset']) 49 | ->withUsername($this->config['username']) 50 | ->withPassword($this->config['password']) 51 | ->withOptions($this->config['options']), 52 | $this->config['size'] 53 | ); 54 | } 55 | } 56 | 57 | public static function getInstance($config = null) 58 | { 59 | if (empty(self::$instance)) { 60 | if (empty($config)) { 61 | throw new RuntimeException('pdo config empty'); 62 | } 63 | if (empty($config['size'])) { 64 | throw new RuntimeException('the size of database connection pools cannot be empty'); 65 | } 66 | self::$instance = new static($config); 67 | } 68 | 69 | return self::$instance; 70 | } 71 | 72 | public function getConnection() 73 | { 74 | return $this->pools->get(); 75 | } 76 | 77 | public function close($connection = null) 78 | { 79 | $this->pools->put($connection); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Redis.php: -------------------------------------------------------------------------------- 1 | 'localhost', 23 | 'port' => 6379, 24 | 'auth' => '', 25 | 'db_index' => 0, 26 | 'time_out' => 1, 27 | 'size' => 64, 28 | ]; 29 | 30 | private static $instance; 31 | 32 | private function __construct(array $config) 33 | { 34 | if (empty($this->pools)) { 35 | $this->config = array_replace_recursive($this->config, $config); 36 | $this->pools = new RedisPool( 37 | (new RedisConfig()) 38 | ->withHost($this->config['host']) 39 | ->withPort($this->config['port']) 40 | ->withAuth($this->config['auth']) 41 | ->withDbIndex($this->config['db_index']) 42 | ->withTimeout($this->config['time_out']), 43 | $this->config['size'] 44 | ); 45 | } 46 | } 47 | 48 | public static function getInstance($config = null, $poolName = 'default') 49 | { 50 | if (empty(self::$instance[$poolName])) { 51 | if (empty($config)) { 52 | throw new RuntimeException('redis config empty'); 53 | } 54 | if (empty($config['size'])) { 55 | throw new RuntimeException('the size of redis connection pools cannot be empty'); 56 | } 57 | self::$instance[$poolName] = new static($config); 58 | } 59 | 60 | return self::$instance[$poolName]; 61 | } 62 | 63 | public function getConnection() 64 | { 65 | return $this->pools->get(); 66 | } 67 | 68 | public function close($connection = null) 69 | { 70 | $this->pools->put($connection); 71 | } 72 | 73 | public function getConfig(): array 74 | { 75 | return $this->config; 76 | } 77 | 78 | public function fill(): void 79 | { 80 | $this->pools->fill(); 81 | } 82 | } 83 | --------------------------------------------------------------------------------