1: <?php
2: 3: 4: 5: 6:
7:
8: namespace Ark\Database;
9:
10: 11: 12:
13: class QueryBuilder
14: {
15: protected $db;
16: protected $statement;
17: protected $params = array();
18: protected $positionParams = array();
19: protected $query = array();
20: protected $sql;
21:
22: 23: 24: 25: 26:
27: public function __construct($db){
28: $this->db = $db;
29: }
30:
31: protected function matchAlias($entry){
32: if(preg_match('#^(.*?)(?i:\s+as\s+|\s+)(.*)$#', $entry, $matches)){
33: return array(
34: $matches[1],
35: $matches[2],
36: );
37: }
38: else{
39: return false;
40: }
41: }
42:
43: protected function splitParts($parts){
44: return preg_split('#\s*,\s*#', trim($parts), -1, PREG_SPLIT_NO_EMPTY);
45: }
46:
47: 48: 49: 50: 51:
52: public function reset(){
53: $this->statement = null;
54: $this->params = array();
55: $this->positionParams = array();
56: $this->query = array();
57: $this->sql = null;
58:
59: return $this;
60: }
61:
62: 63: 64: 65: 66:
67: public function prepare(){
68: if(null === $this->statement){
69: $this->statement = $this->db->prepare($this->getSql());
70: }
71:
72: return $this;
73: }
74:
75: 76: 77: 78:
79: public function bindParam(){
80: $this->prepare();
81: call_user_func_array(array($this->statement, 'bindParam'), func_get_args());
82:
83: return $this;
84: }
85:
86: 87: 88: 89:
90: public function bindValue(){
91: $this->prepare();
92: call_user_func_array(array($this->statement, 'bindValue'), func_get_args());
93:
94: return $this;
95: }
96:
97: 98: 99: 100: 101: 102:
103: public function bindValues($values){
104: $this->prepare();
105:
106: foreach($values as $k=> $value){
107: $this->statement->bindValue($k, $value);
108: }
109:
110: return $this;
111: }
112:
113: 114: 115: 116: 117: 118:
119: public function mergeParams($params){
120: foreach($params as $k => $v){
121: if (is_int($k)) {
122: $this->positionParams[] = $v;
123: } else {
124: $this->params[$k] = $v;
125: }
126: }
127:
128: return $this;
129: }
130:
131: 132: 133: 134: 135: 136: 137: 138: 139:
140: public function select($fields = '*'){
141: if(is_string($fields) && strpos($fields, '(') !== false){
142: $this->query['select'] = $fields;
143: }
144: else{
145: if(!is_array($fields)){
146: $fields = $this->splitParts($fields);
147: }
148: foreach($fields as $k => $field){
149: if(false === strpos($field, '(')){
150: if($alias = $this->matchAlias($field)){
151: $fields[$k] = $this->db->quoteColumn($alias[0]).' AS '.$this->db->quoteColumn($alias[1]);
152: }
153: else{
154: $fields[$k] = $this->db->quoteColumn($field);
155: }
156: }
157: }
158: $this->query['select'] = implode(', ', $fields);
159: }
160:
161: return $this;
162: }
163:
164: 165: 166: 167: 168:
169: public function distinct(){
170: $this->query['distinct'] = true;
171:
172: return $this;
173: }
174:
175: 176: 177: 178: 179: 180: 181: 182: 183:
184: public function from($tables){
185: if(is_string($tables)){
186: $tables = $this->splitParts($tables);
187: }
188: foreach($tables as $k => $table){
189: if($alias = $this->matchAlias($table)){
190: $tables[$k] = $this->db->quoteTable($alias[0]).' AS '.$this->db->quoteTable($alias[1]);
191: }
192: else{
193: $tables[$k] = $this->db->quoteTable($table);
194: }
195: }
196:
197: $this->query['from'] = implode(', ', $tables);
198:
199: return $this;
200: }
201:
202: 203: 204: 205: 206: 207:
208: public function buildConditions($conditions){
209: if(!is_array($conditions)){
210: if(null === $conditions){
211: return '';
212: }
213: return $conditions;
214: }
215: elseif($conditions === array()){
216: return '';
217: }
218:
219: $n = count($conditions);
220: $operator = strtoupper($conditions[0]);
221: if($operator === 'AND' || $operator === 'OR'){
222: $result = array();
223: for($i = 1; $i < $n; $i++){
224: $condition = $this->buildConditions($conditions[$i]);
225: if('' !== $condition){
226: $result[] = '('.$condition.')';
227: }
228: }
229: if($result === array()){
230: return '';
231: }
232: else{
233: return implode(' '.$operator.' ', $result);
234: }
235: }
236:
237: if(!isset($conditions[1], $conditions[2])){
238: return '';
239: }
240:
241: $column = $this->db->quoteColumn($conditions[1]);
242: $values = $conditions[2];
243: if(!is_array($values)){
244: $values = array($values);
245: }
246: if($operator === 'IN' || $operator === 'NOT IN'){
247: if($values === array()){
248: return $operator === 'IN'?'0':'';
249: }
250: foreach($values as $k => $value){
251: $values[$k] = $this->db->quote($value);
252: }
253:
254: return $column.' '.$operator.' ('.implode(',', $values).')';
255: }
256: elseif($operator === 'LIKE' || $operator === 'NOT LIKE' || $operator === 'OR LIKE' || $operator === 'OR NOT LIKE'){
257: if($values === array()){
258: return ($operator === 'LIKE' || $operator === 'OR LIKE')?'0':'';
259: }
260: if($operator === 'LIKE' || $operator === 'NOT LIKE'){
261: $andor = 'AND';
262: }
263: else{
264: $andor = 'OR';
265: $operator = $operator === 'OR LIKE'?'LIKE':'NOT LIKE';
266: }
267:
268: $result = array();
269: foreach($values as $k => $value){
270: $result[] = $column.' '.$operator.' '.$this->db->quote($value);
271: }
272:
273: return implode($andor, $result);
274: }
275: else{
276: throw new Exception('Invalid operator "'.$operator.'"');
277: }
278: }
279:
280: 281: 282: 283: 284: 285: 286:
287: public function where($conditions, $params = array()){
288: $this->mergeParams($params);
289: $this->query['where'] = $this->buildConditions($conditions);
290:
291: return $this;
292: }
293:
294: 295: 296: 297: 298: 299: 300: 301: 302:
303: public function orderBy($fields){
304: if(!is_array($fields)){
305: $fields = $this->splitParts($fields);
306: }
307:
308: foreach($fields as $k => $field){
309: if(preg_match('#^(.*?)\s+(asc|desc)$#i', $field, $matches)){
310: $fields[$k] = $this->db->quoteColumn($matches[1]).' '.$matches[2];
311: }
312: else{
313: $fields[$k] = $this->db->quoteColumn($field);
314: }
315: }
316:
317: $this->query['order'] = implode(',', $fields);
318:
319: return $this;
320: }
321:
322: 323: 324: 325: 326: 327: 328:
329: public function limit($limit, $offset = null){
330: $this->query['limit'] = $limit;
331: if(null !== $offset){
332: $this->offset($offset);
333: }
334:
335: return $this;
336: }
337:
338: 339: 340: 341: 342: 343:
344: public function offset($offset){
345: $this->query['offset'] = $offset;
346:
347: return $this;
348: }
349:
350: 351: 352: 353: 354: 355: 356: 357: 358:
359: protected function anyJoin($type, $table, $conditions = '', $params = array()){
360: $this->mergeParams($params);
361: if($alias = $this->matchAlias($table)){
362: $table = $this->db->quoteTable($alias[0]).' AS '.$this->db->quoteTable($alias[1]);
363: }
364: else{
365: $table = $this->db->quoteTable($table);
366: }
367: $conditions = $this->buildConditions($conditions);
368: if('' !== $conditions){
369: $conditions = ' ON '.$conditions;
370: }
371:
372: if(isset($this->query['join']) && is_string($this->query['join'])){
373: $this->query['join'] = array($this->query['join']);
374: }
375:
376: $this->query['join'][] = $type.' '.$table.' '.$conditions;
377:
378: return $this;
379: }
380:
381: 382: 383: 384: 385: 386: 387: 388:
389: public function join($table, $conditions = '', $params = array()){
390: return $this->anyJoin('JOIN', $table, $conditions, $params);
391: }
392:
393: 394: 395: 396: 397: 398: 399: 400:
401: public function leftJoin($table, $conditions = '', $params = array()){
402: return $this->anyJoin('LEFT JOIN', $table, $conditions, $params);
403: }
404:
405: 406: 407: 408: 409: 410: 411: 412:
413: public function rightJoin($table, $condtions = '', $params = array()){
414: return $this->anyJoin('RIGHT JOIN', $table, $conditions, $params);
415: }
416:
417: 418: 419: 420: 421: 422:
423: public function groupBy($fields){
424: if(!is_array($fields)){
425: $fields = $this->splitParts($fields);
426: }
427:
428: foreach($fields as $k => $field){
429: $fields[$k] = $this->db->quoteColumn($field);
430: }
431:
432: $this->query['group'] = implode(',', $fields);
433:
434: return $this;
435: }
436:
437: 438: 439: 440: 441: 442: 443:
444: public function having($conditions, $params = array()){
445: $this->mergeParams($params);
446: $this->query['having'] = $this->buildConditions($conditions);
447:
448: return $this;
449: }
450:
451: 452: 453: 454: 455: 456:
457: public function union($sql){
458: if(isset($this->query['union']) && is_string($this->query['union'])){
459: $this->query['union'][] = $this->query['union'];
460: }
461:
462: $this->query['union'][] = $sql;
463:
464: return $this;
465: }
466:
467: 468: 469: 470: 471: 472:
473: public function buildQuery($query = null){
474: if(null === $query){
475: $query = $this->query;
476: }
477:
478: $sql = "SELECT ";
479: if(isset($query['distinct']) && $query['distinct']){
480: $sql .= 'DISTINCT ';
481: }
482: $sql .= isset($query['select'])?$query['select']:'*';
483:
484: if(!isset($query['from'])){
485: return false;
486: }
487:
488: $sql .= "\nFROM ".$query['from'];
489: if(isset($query['join'])){
490: $sql .= "\n".(is_array($query['join'])?implode("\n", $query['join']):$query['join']);
491: }
492: if(isset($query['where']) && $query['where'] !== ''){
493: $sql .= "\nWHERE ".$query['where'];
494: }
495: if(isset($query['group'])){
496: $sql .= "\nGROUP BY ".$query['group'];
497: if(isset($query['having'])){
498: $sql .= "\nHAVING ".$query['having'];
499: }
500: }
501: if(isset($query['order'])){
502: $sql .= "\n ORDER BY ".$query['order'];
503: }
504: $limit = isset($query['limit'])?$query['limit']:0;
505: $offset = isset($query['offset'])?$query['offset']:0;
506: $sql = $this->db->buildLimitOffset($sql, $limit, $offset);
507:
508: if(isset($query['union'])){
509: $sql .= "\n".(is_array($query['union'])?implode("\n", $query['union']):$query['union']);
510: }
511:
512: return $sql;
513: }
514:
515: 516: 517: 518: 519: 520:
521: public function setSql($sql){
522: if('' !== $prefix = $this->db->getPrefix()){
523: $sql = preg_replace('#{{(.*?)}}#', $prefix.'\1', $sql);
524: }
525: $this->sql = $sql;
526:
527: return $this;
528: }
529:
530: 531: 532: 533: 534:
535: public function getSql(){
536: if(null === $this->sql){
537: if(!empty($this->query)){
538: $this->setSql($this->buildQuery());
539: }
540: else{
541: return false;
542: }
543: }
544:
545: return $this->sql;
546: }
547:
548: 549: 550:
551: protected function beginQuery(){
552: $autoSlave = $this->db->isAutoSlave();
553: if ($autoSlave) {
554: $this->db->switchConnection('slave');
555: }
556:
557: $this->prepare();
558:
559: if ($autoSlave) {
560: $this->db->switchConnection();
561: }
562:
563: if(false === $this->statement->execute($this->params?$this->params:$this->positionParams)){
564: $info = $this->statement->errorInfo();
565: throw new Exception(sprintf('Statement error #%s: %s', $info[0], $info[2]));
566: }
567: }
568:
569: 570: 571: 572: 573: 574: 575:
576: public function query($params = array()){
577: $this->mergeParams($params);
578: $this->beginQuery();
579: return $this->statement;
580: }
581:
582: 583: 584: 585: 586: 587:
588: public function queryAll($params = array()){
589: $this->mergeParams($params);
590: $this->beginQuery();
591: $rst = $this->statement->fetchAll(\PDO::FETCH_ASSOC);
592: $this->statement->closeCursor();
593: return $rst;
594: }
595:
596: 597: 598: 599: 600: 601:
602: public function queryRow($params = array()){
603: $this->mergeParams($params);
604: $this->beginQuery();
605: $rst = $this->statement->fetch(\PDO::FETCH_ASSOC);
606: $this->statement->closeCursor();
607: return $rst;
608: }
609:
610: 611: 612: 613: 614: 615:
616: public function queryColumn($params = array()){
617: $this->mergeParams($params);
618: $this->beginQuery();
619: $rst = $this->statement->fetchAll(\PDO::FETCH_COLUMN);
620: $this->statement->closeCursor();
621: return $rst;
622: }
623:
624: 625: 626: 627: 628: 629:
630: public function queryValue($params = array()){
631: $this->mergeParams($params);
632: $this->beginQuery();
633: $rst = $this->statement->fetchColumn();
634: $this->statement->closeCursor();
635:
636: return $rst;
637: }
638:
639: 640: 641: 642: 643: 644:
645: public function execute($params = array()){
646: $this->prepare();
647: $this->mergeParams($params);
648:
649: foreach ($this->positionParams as $index => $value) {
650: $this->statement->bindValue($index + 1, $value);
651: }
652:
653: if(false === $rst = $this->statement->execute($this->params)){
654: return false;
655: }
656:
657: return $this->statement->rowCount();
658: }
659:
660: 661: 662: 663: 664: 665: 666:
667: public function insert($table, $values){
668: $keys = array();
669: $params = array();
670: $placeholders = array();
671: foreach($values as $k => $v){
672: $keys[] = $this->db->quoteColumn($k);
673: $params[':'.$k] = $v;
674: $placeholders[] = ':'.$k;
675: }
676: $sql = "INSERT INTO ".$this->db->quoteTable($table).' ('.implode(', ',$keys).') VALUES ('.implode(',', $placeholders).')';
677: $result = $this->setSql($sql)->execute($params);
678: if (!$result) {
679: return false;
680: }
681:
682: return $this->db->lastInsertId();
683: }
684:
685: 686: 687: 688: 689: 690: 691: 692: 693:
694: public function update($table, $values, $conditions = '', $params = array()){
695: $updates = array();
696: if (!is_array($values)) {
697: $values = array($values);
698: }
699: foreach($values as $k => $value){
700: if (is_integer($k)) {
701: $updates[] = $value;
702: } else {
703: $updates[] = $this->db->quoteColumn($k).' = :'.$k;
704: $params[':'.$k] = $value;
705: }
706: }
707:
708: $sql = "UPDATE ".$this->db->quoteTable($table).' SET '.implode(', ', $updates);
709: $conditions = $this->buildConditions($conditions);
710: if('' !== $conditions){
711: $sql .= ' WHERE '.$conditions;
712: }
713:
714: return $this->setSql($sql)->execute($params);
715: }
716:
717: 718: 719: 720: 721: 722: 723: 724: 725:
726: public function delete($table, $conditions = '', $params = array()){
727: $sql = 'DELETE FROM '.$this->db->quoteTable($table);
728: $conditions = $this->buildConditions($conditions);
729: if('' !== $conditions){
730: $sql .= ' WHERE '.$conditions;
731: }
732:
733: return $this->setSql($sql)->execute($params);
734: }
735: }
736: