BaseModel.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Simps.
  5. *
  6. * @link https://simps.io
  7. * @document https://doc.simps.io
  8. * @license https://github.com/simple-swoole/simps/blob/master/LICENSE
  9. */
  10. namespace Simps\DB;
  11. use Exception;
  12. use InvalidArgumentException;
  13. use PDO;
  14. use RuntimeException;
  15. use Swoole\Coroutine;
  16. /*!
  17. * Medoo database framework
  18. * https://medoo.in
  19. * Version 1.7.10
  20. *
  21. * Copyright 2020, Angel Lai
  22. * Released under the MIT license
  23. */
  24. class Raw
  25. {
  26. public $map;
  27. public $value;
  28. }
  29. class BaseModel
  30. {
  31. protected $pool;
  32. /** @var PDO */
  33. protected $pdo;
  34. protected $statement;
  35. protected $logs = [];
  36. protected $logging = false;
  37. protected $debug_mode = false;
  38. protected $guid = 0;
  39. protected $errorInfo;
  40. private $in_transaction = false;
  41. public function __construct($config = null)
  42. {
  43. if (! empty($config)) {
  44. $this->pool = \Simps\DB\PDO::getInstance($config);
  45. } else {
  46. $this->pool = \Simps\DB\PDO::getInstance();
  47. }
  48. }
  49. public function beginTransaction()
  50. {
  51. if ($this->in_transaction) { //嵌套事务
  52. throw new RuntimeException('do not support nested transaction now');
  53. }
  54. $this->realGetConn();
  55. $this->pdo->beginTransaction();
  56. $this->in_transaction = true;
  57. Coroutine::defer(function () {
  58. if ($this->in_transaction) {
  59. $this->rollBack();
  60. }
  61. });
  62. }
  63. public function commit(): void
  64. {
  65. $this->pdo->commit();
  66. $this->in_transaction = false;
  67. $this->release($this->pdo);
  68. }
  69. public function rollBack(): void
  70. {
  71. $this->pdo->rollBack();
  72. $this->in_transaction = false;
  73. $this->release($this->pdo);
  74. }
  75. public function action($actions)
  76. {
  77. if (is_callable($actions)) {
  78. $this->beginTransaction();
  79. try {
  80. $result = $actions($this);
  81. if ($result === false) {
  82. $this->rollBack();
  83. } else {
  84. $this->commit();
  85. }
  86. } catch (Exception $e) {
  87. $this->rollBack();
  88. throw $e;
  89. }
  90. return $result;
  91. }
  92. return false;
  93. }
  94. public function query($query, $map = [])
  95. {
  96. $raw = $this->raw($query, $map);
  97. $query = $this->buildRaw($raw, $map);
  98. return $this->exec($query, $map);
  99. }
  100. public function exec($query, $map = [])
  101. {
  102. $this->realGetConn();
  103. $this->statement = null;
  104. if ($this->debug_mode) {
  105. echo $this->generate($query, $map);
  106. $this->debug_mode = false;
  107. $this->release($this->pdo);
  108. return false;
  109. }
  110. if ($this->logging) {
  111. $this->logs[] = [$query, $map];
  112. } else {
  113. $this->logs = [[$query, $map]];
  114. }
  115. $statement = $this->pdo->prepare($query);
  116. if (! $statement) {
  117. $this->errorInfo = $this->pdo->errorInfo();
  118. $this->statement = null;
  119. $this->release($this->pdo);
  120. return false;
  121. }
  122. $this->statement = $statement;
  123. foreach ($map as $key => $value) {
  124. $statement->bindValue($key, $value[0], $value[1]);
  125. }
  126. $execute = $statement->execute();
  127. $this->errorInfo = $statement->errorInfo();
  128. if (! $execute) {
  129. $this->statement = null;
  130. }
  131. $lastId = $this->pdo->lastInsertId();
  132. if ($lastId != '0' && $lastId != '') {
  133. $this->release($this->pdo);
  134. return $lastId;
  135. }
  136. $this->release($this->pdo);
  137. return $statement;
  138. }
  139. public static function raw($string, $map = [])
  140. {
  141. $raw = new Raw();
  142. $raw->map = $map;
  143. $raw->value = $string;
  144. return $raw;
  145. }
  146. public function quote($string)
  147. {
  148. $this->realGetConn();
  149. $ret = $this->pdo->quote($string);
  150. $this->release($this->pdo);
  151. return $ret;
  152. }
  153. public function create($table, $columns, $options = null)
  154. {
  155. $stack = [];
  156. $tableName = $table;
  157. foreach ($columns as $name => $definition) {
  158. if (is_int($name)) {
  159. $stack[] = preg_replace('/\<([a-zA-Z0-9_]+)\>/i', '"$1"', $definition);
  160. } elseif (is_array($definition)) {
  161. $stack[] = $name . ' ' . implode(' ', $definition);
  162. } elseif (is_string($definition)) {
  163. $stack[] = $name . ' ' . $this->query($definition);
  164. }
  165. }
  166. $table_option = '';
  167. if (is_array($options)) {
  168. $option_stack = [];
  169. foreach ($options as $key => $value) {
  170. if (is_string($value) || is_int($value)) {
  171. $option_stack[] = "{$key} = {$value}";
  172. }
  173. }
  174. $table_option = ' ' . implode(', ', $option_stack);
  175. } elseif (is_string($options)) {
  176. $table_option = ' ' . $options;
  177. }
  178. return $this->exec("CREATE TABLE IF NOT EXISTS {$tableName} (" . implode(', ', $stack) . "){$table_option}");
  179. }
  180. public function drop($table)
  181. {
  182. $tableName = $table;
  183. return $this->exec("DROP TABLE IF EXISTS {$tableName}");
  184. }
  185. public function select($table, $join, $columns = null, $where = null)
  186. {
  187. $map = [];
  188. $result = [];
  189. $column_map = [];
  190. $index = 0;
  191. $column = $where === null ? $join : $columns;
  192. $is_single = (is_string($column) && $column !== '*');
  193. $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map);
  194. $this->columnMap($columns, $column_map, true);
  195. if (! $this->statement) {
  196. return false;
  197. }
  198. if ($columns === '*') {
  199. return $query->fetchAll(PDO::FETCH_ASSOC);
  200. }
  201. while ($data = $query->fetch(PDO::FETCH_ASSOC)) {
  202. $current_stack = [];
  203. $this->dataMap($data, $columns, $column_map, $current_stack, true, $result);
  204. }
  205. if ($is_single) {
  206. $single_result = [];
  207. $result_key = $column_map[$column][0];
  208. foreach ($result as $item) {
  209. $single_result[] = $item[$result_key];
  210. }
  211. return $single_result;
  212. }
  213. return $result;
  214. }
  215. public function insert($table, $datas)
  216. {
  217. $stack = [];
  218. $columns = [];
  219. $fields = [];
  220. $map = [];
  221. if (! isset($datas[0])) {
  222. $datas = [$datas];
  223. }
  224. foreach ($datas as $data) {
  225. foreach ($data as $key => $value) {
  226. $columns[] = $key;
  227. }
  228. }
  229. $columns = array_unique($columns);
  230. foreach ($datas as $data) {
  231. $values = [];
  232. foreach ($columns as $key) {
  233. if ($raw = $this->buildRaw($data[$key], $map)) {
  234. $values[] = $raw;
  235. continue;
  236. }
  237. $map_key = $this->mapKey();
  238. $values[] = $map_key;
  239. if (! isset($data[$key])) {
  240. $map[$map_key] = [null, PDO::PARAM_NULL];
  241. } else {
  242. $value = $data[$key];
  243. $type = gettype($value);
  244. switch ($type) {
  245. case 'array':
  246. $map[$map_key] = [
  247. strpos($key, '[JSON]') === strlen($key) - 6 ?
  248. json_encode($value) :
  249. serialize($value),
  250. PDO::PARAM_STR,
  251. ];
  252. break;
  253. case 'object':
  254. $value = serialize($value);
  255. // no break
  256. case 'NULL':
  257. case 'resource':
  258. case 'boolean':
  259. case 'integer':
  260. case 'double':
  261. case 'string':
  262. $map[$map_key] = $this->typeMap($value, $type);
  263. break;
  264. }
  265. }
  266. }
  267. $stack[] = '(' . implode(', ', $values) . ')';
  268. }
  269. foreach ($columns as $key) {
  270. $fields[] = $this->columnQuote(preg_replace('/(\\s*\\[JSON\\]$)/i', '', $key));
  271. }
  272. return $this->exec(
  273. 'INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode(
  274. ', ',
  275. $stack
  276. ),
  277. $map
  278. );
  279. }
  280. public function update($table, $data, $where = null)
  281. {
  282. $fields = [];
  283. $map = [];
  284. foreach ($data as $key => $value) {
  285. $column = $this->columnQuote(preg_replace('/(\\s*\\[(JSON|\\+|\\-|\\*|\\/)\\]$)/i', '', $key));
  286. if ($raw = $this->buildRaw($value, $map)) {
  287. $fields[] = $column . ' = ' . $raw;
  288. continue;
  289. }
  290. $map_key = $this->mapKey();
  291. preg_match('/(?<column>[a-zA-Z0-9_]+)(\[(?<operator>\+|\-|\*|\/)\])?/i', $key, $match);
  292. if (isset($match['operator'])) {
  293. if (is_numeric($value)) {
  294. $fields[] = $column . ' = ' . $column . ' ' . $match['operator'] . ' ' . $value;
  295. }
  296. } else {
  297. $fields[] = $column . ' = ' . $map_key;
  298. $type = gettype($value);
  299. switch ($type) {
  300. case 'array':
  301. $map[$map_key] = [
  302. strpos($key, '[JSON]') === strlen($key) - 6 ?
  303. json_encode($value) :
  304. serialize($value),
  305. PDO::PARAM_STR,
  306. ];
  307. break;
  308. case 'object':
  309. $value = serialize($value);
  310. // no break
  311. case 'NULL':
  312. case 'resource':
  313. case 'boolean':
  314. case 'integer':
  315. case 'double':
  316. case 'string':
  317. $map[$map_key] = $this->typeMap($value, $type);
  318. break;
  319. }
  320. }
  321. }
  322. return $this->exec(
  323. 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $fields) . $this->whereClause($where, $map),
  324. $map
  325. );
  326. }
  327. public function delete($table, $where)
  328. {
  329. $map = [];
  330. return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where, $map), $map);
  331. }
  332. public function replace($table, $columns, $where = null)
  333. {
  334. if (! is_array($columns) || empty($columns)) {
  335. return false;
  336. }
  337. $map = [];
  338. $stack = [];
  339. foreach ($columns as $column => $replacements) {
  340. if (is_array($replacements)) {
  341. foreach ($replacements as $old => $new) {
  342. $map_key = $this->mapKey();
  343. $stack[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote(
  344. $column
  345. ) . ', ' . $map_key . 'a, ' . $map_key . 'b)';
  346. $map[$map_key . 'a'] = [$old, PDO::PARAM_STR];
  347. $map[$map_key . 'b'] = [$new, PDO::PARAM_STR];
  348. }
  349. }
  350. }
  351. if (! empty($stack)) {
  352. return $this->exec(
  353. 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause(
  354. $where,
  355. $map
  356. ),
  357. $map
  358. );
  359. }
  360. return false;
  361. }
  362. public function get($table, $join = null, $columns = null, $where = null)
  363. {
  364. $map = [];
  365. $result = [];
  366. $column_map = [];
  367. $current_stack = [];
  368. if ($where === null) {
  369. $column = $join;
  370. unset($columns['LIMIT']);
  371. } else {
  372. $column = $columns;
  373. unset($where['LIMIT']);
  374. }
  375. $is_single = (is_string($column) && $column !== '*');
  376. $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where) . ' LIMIT 1', $map);
  377. if (! $this->statement) {
  378. return false;
  379. }
  380. $data = $query->fetchAll(PDO::FETCH_ASSOC);
  381. if (isset($data[0])) {
  382. if ($column === '*') {
  383. return $data[0];
  384. }
  385. $this->columnMap($columns, $column_map, true);
  386. $this->dataMap($data[0], $columns, $column_map, $current_stack, true, $result);
  387. if ($is_single) {
  388. return $result[0][$column_map[$column][0]];
  389. }
  390. return $result[0];
  391. }
  392. }
  393. public function has($table, $join, $where = null)
  394. {
  395. $map = [];
  396. $column = null;
  397. $query = $this->exec(
  398. 'SELECT EXISTS(' . $this->selectContext($table, $map, $join, $column, $where, 1) . ')',
  399. $map
  400. );
  401. if (! $this->statement) {
  402. return false;
  403. }
  404. $result = $query->fetchColumn();
  405. return $result === '1' || $result === 1 || $result === true;
  406. }
  407. public function rand($table, $join = null, $columns = null, $where = null)
  408. {
  409. $order = 'RANDOM()';
  410. $order_raw = $this->raw($order);
  411. if ($where === null) {
  412. if ($columns === null) {
  413. $columns = [
  414. 'ORDER' => $order_raw,
  415. ];
  416. } else {
  417. $column = $join;
  418. unset($columns['ORDER']);
  419. $columns['ORDER'] = $order_raw;
  420. }
  421. } else {
  422. unset($where['ORDER']);
  423. $where['ORDER'] = $order_raw;
  424. }
  425. return $this->select($table, $join, $columns, $where);
  426. }
  427. public function count($table, $join = null, $column = null, $where = null)
  428. {
  429. return $this->aggregate('count', $table, $join, $column, $where);
  430. }
  431. public function avg($table, $join, $column = null, $where = null)
  432. {
  433. return $this->aggregate('avg', $table, $join, $column, $where);
  434. }
  435. public function max($table, $join, $column = null, $where = null)
  436. {
  437. return $this->aggregate('max', $table, $join, $column, $where);
  438. }
  439. public function min($table, $join, $column = null, $where = null)
  440. {
  441. return $this->aggregate('min', $table, $join, $column, $where);
  442. }
  443. public function sum($table, $join, $column = null, $where = null)
  444. {
  445. return $this->aggregate('sum', $table, $join, $column, $where);
  446. }
  447. public function debug()
  448. {
  449. $this->debug_mode = true;
  450. return $this;
  451. }
  452. public function error()
  453. {
  454. return $this->errorInfo;
  455. }
  456. public function last()
  457. {
  458. $log = end($this->logs);
  459. return $this->generate($log[0], $log[1]);
  460. }
  461. public function log()
  462. {
  463. return array_map(
  464. function ($log) {
  465. return $this->generate($log[0], $log[1]);
  466. },
  467. $this->logs
  468. );
  469. }
  470. public function info()
  471. {
  472. $output = [
  473. 'server' => 'SERVER_INFO',
  474. 'driver' => 'DRIVER_NAME',
  475. 'client' => 'CLIENT_VERSION',
  476. 'version' => 'SERVER_VERSION',
  477. 'connection' => 'CONNECTION_STATUS',
  478. ];
  479. foreach ($output as $key => $value) {
  480. $output[$key] = @$this->pdo->getAttribute(constant('PDO::ATTR_' . $value));
  481. }
  482. return $output;
  483. }
  484. public function release($connection = null)
  485. {
  486. if ($connection === null) {
  487. $this->in_transaction = false;
  488. }
  489. if (! $this->in_transaction) {
  490. $this->pool->close($connection);
  491. return true;
  492. }
  493. return false;
  494. }
  495. protected function generate($query, $map)
  496. {
  497. $query = preg_replace(
  498. '/"([a-zA-Z0-9_]+)"/i',
  499. '`$1`',
  500. $query
  501. );
  502. foreach ($map as $key => $value) {
  503. if ($value[1] === PDO::PARAM_STR) {
  504. $replace = $this->quote($value[0]);
  505. } elseif ($value[1] === PDO::PARAM_NULL) {
  506. $replace = 'NULL';
  507. } elseif ($value[1] === PDO::PARAM_LOB) {
  508. $replace = '{LOB_DATA}';
  509. } else {
  510. $replace = $value[0];
  511. }
  512. $query = str_replace($key, $replace, $query);
  513. }
  514. return $query;
  515. }
  516. protected function isRaw($object)
  517. {
  518. return $object instanceof Raw;
  519. }
  520. protected function buildRaw($raw, &$map)
  521. {
  522. if (! $this->isRaw($raw)) {
  523. return false;
  524. }
  525. $query = preg_replace_callback(
  526. '/(([`\']).*?)?((FROM|TABLE|INTO|UPDATE|JOIN)\s*)?\<(([a-zA-Z0-9_]+)(\.[a-zA-Z0-9_]+)?)\>(.*?\2)?/i',
  527. function ($matches) {
  528. if (! empty($matches[2]) && isset($matches[8])) {
  529. return $matches[0];
  530. }
  531. if (! empty($matches[4])) {
  532. return $matches[1] . $matches[4] . ' ' . $this->tableQuote($matches[5]);
  533. }
  534. return $matches[1] . $this->columnQuote($matches[5]);
  535. },
  536. $raw->value
  537. );
  538. $raw_map = $raw->map;
  539. if (! empty($raw_map)) {
  540. foreach ($raw_map as $key => $value) {
  541. $map[$key] = $this->typeMap($value, gettype($value));
  542. }
  543. }
  544. return $query;
  545. }
  546. protected function tableQuote($table)
  547. {
  548. if (! preg_match('/^[a-zA-Z0-9_]+$/i', $table)) {
  549. throw new InvalidArgumentException("Incorrect table name \"{$table}\"");
  550. }
  551. return '"' . $table . '"';
  552. }
  553. protected function mapKey()
  554. {
  555. return ':MeDoO_' . $this->guid++ . '_mEdOo';
  556. }
  557. protected function typeMap($value, $type)
  558. {
  559. $map = [
  560. 'NULL' => PDO::PARAM_NULL,
  561. 'integer' => PDO::PARAM_INT,
  562. 'double' => PDO::PARAM_STR,
  563. 'boolean' => PDO::PARAM_BOOL,
  564. 'string' => PDO::PARAM_STR,
  565. 'object' => PDO::PARAM_STR,
  566. 'resource' => PDO::PARAM_LOB,
  567. ];
  568. if ($type === 'boolean') {
  569. $value = ($value ? '1' : '0');
  570. } elseif ($type === 'NULL') {
  571. $value = null;
  572. }
  573. return [$value, $map[$type]];
  574. }
  575. protected function columnQuote($string)
  576. {
  577. if (! preg_match('/^[a-zA-Z0-9_]+(\.?[a-zA-Z0-9_]+)?$/i', $string)) {
  578. throw new InvalidArgumentException("Incorrect column name \"{$string}\"");
  579. }
  580. if (strpos($string, '.') !== false) {
  581. return '"' . str_replace('.', '"."', $string) . '"';
  582. }
  583. return '"' . $string . '"';
  584. }
  585. protected function columnPush(&$columns, &$map, $root, $is_join = false)
  586. {
  587. if ($columns === '*') {
  588. return $columns;
  589. }
  590. $stack = [];
  591. if (is_string($columns)) {
  592. $columns = [$columns];
  593. }
  594. foreach ($columns as $key => $value) {
  595. if (! is_int($key) && is_array($value) && $root && count(array_keys($columns)) === 1) {
  596. $stack[] = $this->columnQuote($key);
  597. $stack[] = $this->columnPush($value, $map, false, $is_join);
  598. } elseif (is_array($value)) {
  599. $stack[] = $this->columnPush($value, $map, false, $is_join);
  600. } elseif (! is_int($key) && $raw = $this->buildRaw($value, $map)) {
  601. preg_match('/(?<column>[a-zA-Z0-9_\.]+)(\s*\[(?<type>(String|Bool|Int|Number))\])?/i', $key, $match);
  602. $stack[] = $raw . ' AS ' . $this->columnQuote($match['column']);
  603. } elseif (is_int($key) && is_string($value)) {
  604. if ($is_join && strpos($value, '*') !== false) {
  605. throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table');
  606. }
  607. preg_match(
  608. '/(?<column>[a-zA-Z0-9_\.]+)(?:\s*\((?<alias>[a-zA-Z0-9_]+)\))?(?:\s*\[(?<type>(?:String|Bool|Int|Number|Object|JSON))\])?/i',
  609. $value,
  610. $match
  611. );
  612. if (! empty($match['alias'])) {
  613. $stack[] = $this->columnQuote($match['column']) . ' AS ' . $this->columnQuote($match['alias']);
  614. $columns[$key] = $match['alias'];
  615. if (! empty($match['type'])) {
  616. $columns[$key] .= ' [' . $match['type'] . ']';
  617. }
  618. } else {
  619. $stack[] = $this->columnQuote($match['column']);
  620. }
  621. }
  622. }
  623. return implode(',', $stack);
  624. }
  625. protected function arrayQuote($array)
  626. {
  627. $stack = [];
  628. foreach ($array as $value) {
  629. $stack[] = is_int($value) ? $value : $this->pdo->quote($value);
  630. }
  631. return implode(',', $stack);
  632. }
  633. protected function innerConjunct($data, $map, $conjunctor, $outer_conjunctor)
  634. {
  635. $stack = [];
  636. foreach ($data as $value) {
  637. $stack[] = '(' . $this->dataImplode($value, $map, $conjunctor) . ')';
  638. }
  639. return implode($outer_conjunctor . ' ', $stack);
  640. }
  641. protected function dataImplode($data, &$map, $conjunctor)
  642. {
  643. $stack = [];
  644. foreach ($data as $key => $value) {
  645. $type = gettype($value);
  646. if (
  647. $type === 'array' &&
  648. preg_match('/^(AND|OR)(\\s+#.*)?$/', $key, $relation_match)
  649. ) {
  650. $relationship = $relation_match[1];
  651. $stack[] = $value !== array_keys(array_keys($value)) ?
  652. '(' . $this->dataImplode($value, $map, ' ' . $relationship) . ')' :
  653. '(' . $this->innerConjunct($value, $map, ' ' . $relationship, $conjunctor) . ')';
  654. continue;
  655. }
  656. $map_key = $this->mapKey();
  657. if (
  658. is_int($key) &&
  659. preg_match('/([a-zA-Z0-9_\.]+)\[(?<operator>\>\=?|\<\=?|\!?\=)\]([a-zA-Z0-9_\.]+)/i', $value, $match)
  660. ) {
  661. $stack[] = $this->columnQuote($match[1]) . ' ' . $match['operator'] . ' ' . $this->columnQuote(
  662. $match[3]
  663. );
  664. } else {
  665. preg_match(
  666. '/([a-zA-Z0-9_\.]+)(\[(?<operator>\>\=?|\<\=?|\!|\<\>|\>\<|\!?~|REGEXP)\])?/i',
  667. $key,
  668. $match
  669. );
  670. $column = $this->columnQuote($match[1]);
  671. if (isset($match['operator'])) {
  672. $operator = $match['operator'];
  673. if (in_array($operator, ['>', '>=', '<', '<='])) {
  674. $condition = $column . ' ' . $operator . ' ';
  675. if (is_numeric($value)) {
  676. $condition .= $map_key;
  677. $map[$map_key] = [$value, is_float($value) ? PDO::PARAM_STR : PDO::PARAM_INT];
  678. } elseif ($raw = $this->buildRaw($value, $map)) {
  679. $condition .= $raw;
  680. } else {
  681. $condition .= $map_key;
  682. $map[$map_key] = [$value, PDO::PARAM_STR];
  683. }
  684. $stack[] = $condition;
  685. } elseif ($operator === '!') {
  686. switch ($type) {
  687. case 'NULL':
  688. $stack[] = $column . ' IS NOT NULL';
  689. break;
  690. case 'array':
  691. $placeholders = [];
  692. foreach ($value as $index => $item) {
  693. $stack_key = $map_key . $index . '_i';
  694. $placeholders[] = $stack_key;
  695. $map[$stack_key] = $this->typeMap($item, gettype($item));
  696. }
  697. $stack[] = $column . ' NOT IN (' . implode(', ', $placeholders) . ')';
  698. break;
  699. case 'object':
  700. if ($raw = $this->buildRaw($value, $map)) {
  701. $stack[] = $column . ' != ' . $raw;
  702. }
  703. break;
  704. case 'integer':
  705. case 'double':
  706. case 'boolean':
  707. case 'string':
  708. $stack[] = $column . ' != ' . $map_key;
  709. $map[$map_key] = $this->typeMap($value, $type);
  710. break;
  711. }
  712. } elseif ($operator === '~' || $operator === '!~') {
  713. if ($type !== 'array') {
  714. $value = [$value];
  715. }
  716. $connector = ' OR ';
  717. $data = array_values($value);
  718. if (is_array($data[0])) {
  719. if (isset($value['AND']) || isset($value['OR'])) {
  720. $connector = ' ' . array_keys($value)[0] . ' ';
  721. $value = $data[0];
  722. }
  723. }
  724. $like_clauses = [];
  725. foreach ($value as $index => $item) {
  726. $item = strval($item);
  727. if (! preg_match('/(\[.+\]|[\*\?\!\%#^-_]|%.+|.+%)/', $item)) {
  728. $item = '%' . $item . '%';
  729. }
  730. $like_clauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $map_key . 'L' . $index;
  731. $map[$map_key . 'L' . $index] = [$item, PDO::PARAM_STR];
  732. }
  733. $stack[] = '(' . implode($connector, $like_clauses) . ')';
  734. } elseif ($operator === '<>' || $operator === '><') {
  735. if ($type === 'array') {
  736. if ($operator === '><') {
  737. $column .= ' NOT';
  738. }
  739. $stack[] = '(' . $column . ' BETWEEN ' . $map_key . 'a AND ' . $map_key . 'b)';
  740. $data_type = (is_numeric($value[0]) && is_numeric(
  741. $value[1]
  742. )) ? PDO::PARAM_INT : PDO::PARAM_STR;
  743. $map[$map_key . 'a'] = [$value[0], $data_type];
  744. $map[$map_key . 'b'] = [$value[1], $data_type];
  745. }
  746. } elseif ($operator === 'REGEXP') {
  747. $stack[] = $column . ' REGEXP ' . $map_key;
  748. $map[$map_key] = [$value, PDO::PARAM_STR];
  749. }
  750. } else {
  751. switch ($type) {
  752. case 'NULL':
  753. $stack[] = $column . ' IS NULL';
  754. break;
  755. case 'array':
  756. $placeholders = [];
  757. foreach ($value as $index => $item) {
  758. $stack_key = $map_key . $index . '_i';
  759. $placeholders[] = $stack_key;
  760. $map[$stack_key] = $this->typeMap($item, gettype($item));
  761. }
  762. $stack[] = $column . ' IN (' . implode(', ', $placeholders) . ')';
  763. break;
  764. case 'object':
  765. if ($raw = $this->buildRaw($value, $map)) {
  766. $stack[] = $column . ' = ' . $raw;
  767. }
  768. break;
  769. case 'integer':
  770. case 'double':
  771. case 'boolean':
  772. case 'string':
  773. $stack[] = $column . ' = ' . $map_key;
  774. $map[$map_key] = $this->typeMap($value, $type);
  775. break;
  776. }
  777. }
  778. }
  779. }
  780. return implode($conjunctor . ' ', $stack);
  781. }
  782. protected function whereClause($where, &$map)
  783. {
  784. $where_clause = '';
  785. if (is_array($where)) {
  786. $where_keys = array_keys($where);
  787. $conditions = array_diff_key(
  788. $where,
  789. array_flip(
  790. ['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH']
  791. )
  792. );
  793. if (! empty($conditions)) {
  794. $where_clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND');
  795. }
  796. if (isset($where['GROUP'])) {
  797. $GROUP = $where['GROUP'];
  798. if (is_array($GROUP)) {
  799. $stack = [];
  800. foreach ($GROUP as $column => $value) {
  801. $stack[] = $this->columnQuote($value);
  802. }
  803. $where_clause .= ' GROUP BY ' . implode(',', $stack);
  804. } elseif ($raw = $this->buildRaw($GROUP, $map)) {
  805. $where_clause .= ' GROUP BY ' . $raw;
  806. } else {
  807. $where_clause .= ' GROUP BY ' . $this->columnQuote($GROUP);
  808. }
  809. if (isset($where['HAVING'])) {
  810. if ($raw = $this->buildRaw($where['HAVING'], $map)) {
  811. $where_clause .= ' HAVING ' . $raw;
  812. } else {
  813. $where_clause .= ' HAVING ' . $this->dataImplode($where['HAVING'], $map, ' AND');
  814. }
  815. }
  816. }
  817. if (isset($where['ORDER'])) {
  818. $ORDER = $where['ORDER'];
  819. if (is_array($ORDER)) {
  820. $stack = [];
  821. foreach ($ORDER as $column => $value) {
  822. if (is_array($value)) {
  823. $stack[] = 'FIELD(' . $this->columnQuote($column) . ', ' . $this->arrayQuote($value) . ')';
  824. } elseif ($value === 'ASC' || $value === 'DESC') {
  825. $stack[] = $this->columnQuote($column) . ' ' . $value;
  826. } elseif (is_int($column)) {
  827. $stack[] = $this->columnQuote($value);
  828. }
  829. }
  830. $where_clause .= ' ORDER BY ' . implode(',', $stack);
  831. } elseif ($raw = $this->buildRaw($ORDER, $map)) {
  832. $where_clause .= ' ORDER BY ' . $raw;
  833. } else {
  834. $where_clause .= ' ORDER BY ' . $this->columnQuote($ORDER);
  835. }
  836. }
  837. if (isset($where['LIMIT'])) {
  838. $LIMIT = $where['LIMIT'];
  839. if (is_numeric($LIMIT)) {
  840. $where_clause .= ' LIMIT ' . $LIMIT;
  841. } elseif (
  842. is_array($LIMIT) &&
  843. is_numeric($LIMIT[0]) &&
  844. is_numeric($LIMIT[1])
  845. ) {
  846. $where_clause .= ' LIMIT ' . $LIMIT[1] . ' OFFSET ' . $LIMIT[0];
  847. }
  848. }
  849. } elseif ($raw = $this->buildRaw($where, $map)) {
  850. $where_clause .= ' ' . $raw;
  851. }
  852. return $where_clause;
  853. }
  854. protected function selectContext($table, &$map, $join, &$columns = null, $where = null, $column_fn = null)
  855. {
  856. preg_match('/(?<table>[a-zA-Z0-9_]+)\s*\((?<alias>[a-zA-Z0-9_]+)\)/i', $table, $table_match);
  857. if (isset($table_match['table'], $table_match['alias'])) {
  858. $table = $this->tableQuote($table_match['table']);
  859. $table_query = $table . ' AS ' . $this->tableQuote($table_match['alias']);
  860. } else {
  861. $table = $this->tableQuote($table);
  862. $table_query = $table;
  863. }
  864. $is_join = false;
  865. $join_key = is_array($join) ? array_keys($join) : null;
  866. if (
  867. isset($join_key[0]) &&
  868. strpos((string) $join_key[0], '[') === 0
  869. ) {
  870. $is_join = true;
  871. $table_query .= ' ' . $this->buildJoin($table, $join);
  872. } else {
  873. if (is_null($columns)) {
  874. if (
  875. ! is_null($where) ||
  876. (is_array($join) && isset($column_fn))
  877. ) {
  878. $where = $join;
  879. $columns = null;
  880. } else {
  881. $where = null;
  882. $columns = $join;
  883. }
  884. } else {
  885. $where = $columns;
  886. $columns = $join;
  887. }
  888. }
  889. if (isset($column_fn)) {
  890. if ($column_fn === 1) {
  891. $column = '1';
  892. if (is_null($where)) {
  893. $where = $columns;
  894. }
  895. } elseif ($raw = $this->buildRaw($column_fn, $map)) {
  896. $column = $raw;
  897. } else {
  898. if (empty($columns) || $this->isRaw($columns)) {
  899. $columns = '*';
  900. $where = $join;
  901. }
  902. $column = $column_fn . '(' . $this->columnPush($columns, $map, true) . ')';
  903. }
  904. } else {
  905. $column = $this->columnPush($columns, $map, true, $is_join);
  906. }
  907. return 'SELECT ' . $column . ' FROM ' . $table_query . $this->whereClause($where, $map);
  908. }
  909. protected function buildJoin($table, $join)
  910. {
  911. $table_join = [];
  912. $join_array = [
  913. '>' => 'LEFT',
  914. '<' => 'RIGHT',
  915. '<>' => 'FULL',
  916. '><' => 'INNER',
  917. ];
  918. foreach ($join as $sub_table => $relation) {
  919. preg_match(
  920. '/(\[(?<join>\<\>?|\>\<?)\])?(?<table>[a-zA-Z0-9_]+)\s?(\((?<alias>[a-zA-Z0-9_]+)\))?/',
  921. $sub_table,
  922. $match
  923. );
  924. if ($match['join'] !== '' && $match['table'] !== '') {
  925. if (is_string($relation)) {
  926. $relation = 'USING ("' . $relation . '")';
  927. }
  928. if (is_array($relation)) {
  929. // For ['column1', 'column2']
  930. if (isset($relation[0])) {
  931. $relation = 'USING ("' . implode('", "', $relation) . '")';
  932. } else {
  933. $joins = [];
  934. foreach ($relation as $key => $value) {
  935. $joins[] = (
  936. strpos($key, '.') > 0 ?
  937. // For ['tableB.column' => 'column']
  938. $this->columnQuote($key) :
  939. // For ['column1' => 'column2']
  940. $table . '."' . $key . '"'
  941. ) .
  942. ' = ' .
  943. $this->tableQuote(
  944. isset($match['alias']) ? $match['alias'] : $match['table']
  945. ) . '."' . $value . '"';
  946. }
  947. $relation = 'ON ' . implode(' AND ', $joins);
  948. }
  949. }
  950. $table_name = $this->tableQuote($match['table']) . ' ';
  951. if (isset($match['alias'])) {
  952. $table_name .= 'AS ' . $this->tableQuote($match['alias']) . ' ';
  953. }
  954. $table_join[] = $join_array[$match['join']] . ' JOIN ' . $table_name . $relation;
  955. }
  956. }
  957. return implode(' ', $table_join);
  958. }
  959. protected function columnMap($columns, &$stack, $root)
  960. {
  961. if ($columns === '*') {
  962. return $stack;
  963. }
  964. foreach ($columns as $key => $value) {
  965. if (is_int($key)) {
  966. preg_match(
  967. '/([a-zA-Z0-9_]+\.)?(?<column>[a-zA-Z0-9_]+)(?:\s*\((?<alias>[a-zA-Z0-9_]+)\))?(?:\s*\[(?<type>(?:String|Bool|Int|Number|Object|JSON))\])?/i',
  968. $value,
  969. $key_match
  970. );
  971. $column_key = ! empty($key_match['alias']) ?
  972. $key_match['alias'] :
  973. $key_match['column'];
  974. if (isset($key_match['type'])) {
  975. $stack[$value] = [$column_key, $key_match['type']];
  976. } else {
  977. $stack[$value] = [$column_key, 'String'];
  978. }
  979. } elseif ($this->isRaw($value)) {
  980. preg_match(
  981. '/([a-zA-Z0-9_]+\.)?(?<column>[a-zA-Z0-9_]+)(\s*\[(?<type>(String|Bool|Int|Number))\])?/i',
  982. $key,
  983. $key_match
  984. );
  985. $column_key = $key_match['column'];
  986. if (isset($key_match['type'])) {
  987. $stack[$key] = [$column_key, $key_match['type']];
  988. } else {
  989. $stack[$key] = [$column_key, 'String'];
  990. }
  991. } elseif (! is_int($key) && is_array($value)) {
  992. if ($root && count(array_keys($columns)) === 1) {
  993. $stack[$key] = [$key, 'String'];
  994. }
  995. $this->columnMap($value, $stack, false);
  996. }
  997. }
  998. return $stack;
  999. }
  1000. protected function dataMap($data, $columns, $column_map, &$stack, $root, &$result)
  1001. {
  1002. if ($root) {
  1003. $columns_key = array_keys($columns);
  1004. if (count($columns_key) === 1 && is_array($columns[$columns_key[0]])) {
  1005. $index_key = array_keys($columns)[0];
  1006. $data_key = preg_replace('/^[a-zA-Z0-9_]+\\./i', '', $index_key);
  1007. $current_stack = [];
  1008. foreach ($data as $item) {
  1009. $this->dataMap($data, $columns[$index_key], $column_map, $current_stack, false, $result);
  1010. $index = $data[$data_key];
  1011. $result[$index] = $current_stack;
  1012. }
  1013. } else {
  1014. $current_stack = [];
  1015. $this->dataMap($data, $columns, $column_map, $current_stack, false, $result);
  1016. $result[] = $current_stack;
  1017. }
  1018. return;
  1019. }
  1020. foreach ($columns as $key => $value) {
  1021. $isRaw = $this->isRaw($value);
  1022. if (is_int($key) || $isRaw) {
  1023. $map = $column_map[$isRaw ? $key : $value];
  1024. $column_key = $map[0];
  1025. $item = $data[$column_key];
  1026. if (isset($map[1])) {
  1027. if ($isRaw && in_array($map[1], ['Object', 'JSON'])) {
  1028. continue;
  1029. }
  1030. if (is_null($item)) {
  1031. $stack[$column_key] = null;
  1032. continue;
  1033. }
  1034. switch ($map[1]) {
  1035. case 'Number':
  1036. $stack[$column_key] = (float) $item;
  1037. break;
  1038. case 'Int':
  1039. $stack[$column_key] = (int) $item;
  1040. break;
  1041. case 'Bool':
  1042. $stack[$column_key] = (bool) $item;
  1043. break;
  1044. case 'Object':
  1045. $stack[$column_key] = unserialize($item);
  1046. break;
  1047. case 'JSON':
  1048. $stack[$column_key] = json_decode($item, true);
  1049. break;
  1050. case 'String':
  1051. $stack[$column_key] = $item;
  1052. break;
  1053. }
  1054. } else {
  1055. $stack[$column_key] = $item;
  1056. }
  1057. } else {
  1058. $current_stack = [];
  1059. $this->dataMap($data, $value, $column_map, $current_stack, false, $result);
  1060. $stack[$key] = $current_stack;
  1061. }
  1062. }
  1063. }
  1064. private function realGetConn()
  1065. {
  1066. if (! $this->in_transaction) {
  1067. $this->pdo = $this->pool->getConnection();
  1068. $this->pdo->exec('SET SQL_MODE=ANSI_QUOTES');
  1069. }
  1070. }
  1071. private function aggregate($type, $table, $join = null, $column = null, $where = null)
  1072. {
  1073. $map = [];
  1074. $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, strtoupper($type)), $map);
  1075. if (! $this->statement) {
  1076. return false;
  1077. }
  1078. $number = $query->fetchColumn();
  1079. return is_numeric($number) ? $number + 0 : $number;
  1080. }
  1081. }