1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie\ActiveRecord;
13
14 use ICanBoogie\DateTime;
15 use ICanBoogie\Prototype\MethodNotDefined;
16
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
35 class Query extends \ICanBoogie\Object implements \IteratorAggregate
36 {
37 const LIMIT_MAX = '18446744073709551615';
38
39 protected $select;
40 protected $join;
41
42 protected $conditions = [];
43 protected $conditions_args = [];
44
45 protected $group;
46 protected $order;
47 protected $having;
48 protected $having_args = [];
49
50 protected $offset;
51 protected $limit;
52
53 protected $mode;
54
55 56 57 58 59
60 protected $model;
61
62 63 64 65 66
67 public function __construct(Model $model)
68 {
69 $this->model = $model;
70 }
71
72 73 74
75 public function __get($property)
76 {
77 if ($property == 'model')
78 {
79 return $this->model;
80 }
81
82 $scopes = $this->get_model_scope();
83
84 if (in_array($property, $scopes))
85 {
86 return $this->model->scope($property, [ $this ]);
87 }
88
89 return parent::__get($property);
90 }
91
92 93 94
95 public function __call($method, $arguments)
96 {
97 if ($method === 'and')
98 {
99 return call_user_func_array([ $this, 'where' ], $arguments);
100 }
101
102 if (strpos($method, 'filter_by_') === 0)
103 {
104 return $this->dynamic_filter(substr($method, 10), $arguments);
105 }
106
107 $scopes = $this->get_model_scope();
108
109 if (in_array($method, $scopes))
110 {
111 array_unshift($arguments, $this);
112
113 return $this->model->scope($method, $arguments);
114 }
115
116 try
117 {
118 return parent::__call($method, $arguments);
119 }
120 catch (MethodNotDefined $e)
121 {
122 throw new ScopeNotDefined($method, $this->model, 500, $e);
123 }
124 }
125
126 127 128 129 130
131 public function __toString()
132 {
133 return $this->model->resolve_statement
134 (
135 'SELECT ' . ($this->select ? $this->select : '*') . ' FROM {self_and_related}' . $this->build()
136 );
137 }
138
139 140 141 142 143
144 static protected $scopes_by_classes = [];
145
146 147 148 149 150 151 152
153 protected function get_model_scope()
154 {
155 $class = get_class($this->model);
156
157 if (isset(self::$scopes_by_classes[$class]))
158 {
159 return self::$scopes_by_classes[$class];
160 }
161
162 $reflexion = new \ReflectionClass($class);
163 $methods = $reflexion->getMethods(\ReflectionMethod::IS_PROTECTED);
164
165 $scopes = [];
166
167 foreach ($methods as $method)
168 {
169 $name = $method->name;
170
171 if (strpos($name, 'scope_') !== 0)
172 {
173 continue;
174 }
175
176 $scopes[] = substr($name, 6);
177 }
178
179 return self::$scopes_by_classes[$class] = $scopes;
180 }
181
182 183 184 185 186 187 188
189 public function select($expression)
190 {
191 $this->select = $expression;
192
193 return $this;
194 }
195
196 197 198 199 200 201 202 203
204 public function joins($expression)
205 {
206 if ($expression{0} == ':')
207 {
208 $primary = $this->model->primary;
209
210 $model = get_model(substr($expression, 1));
211 $model_schema = $model->extended_schema;
212
213 if (is_array($primary))
214 {
215 foreach ($primary as $column)
216 {
217 if (isset($model_schema['fields'][$column]))
218 {
219 $primary = $column;
220
221 break;
222 }
223 }
224 }
225 else if (empty($model_schema['fields'][$primary]))
226 {
227 $primary = $model_schema['primary'];
228
229 if (is_array($primary))
230 {
231 $primary = current($primary);
232 }
233 }
234
235 $expression = $model->resolve_statement("INNER JOIN `{self}` AS `{alias}` USING(`{$primary}`)");
236 }
237
238 $this->join .= ($this->join ? ' ' : '') . $expression;
239
240 return $this;
241 }
242
243 244 245 246 247 248 249
250 private function defered_parse_conditions()
251 {
252 $trace = debug_backtrace(false);
253 $args = $trace[1]['args'];
254
255 $conditions = array_shift($args);
256
257 if (is_array($conditions))
258 {
259 $c = '';
260 $conditions_args = [];
261
262 foreach ($conditions as $column => $arg)
263 {
264 if (is_array($arg))
265 {
266 $joined = '';
267
268 foreach ($arg as $value)
269 {
270 $joined .= ',' . (is_numeric($value) ? $value : $this->model->quote($value));
271 }
272
273 $joined = substr($joined, 1);
274
275 $c .= ' AND `' . ($column{0} == '!' ? substr($column, 1) . '` NOT' : $column . '`') . ' IN(' . $joined . ')';
276 }
277 else
278 {
279 $conditions_args[] = $arg;
280
281 $c .= ' AND `' . ($column{0} == '!' ? substr($column, 1) . '` !' : $column . '` ') . '= ?';
282 }
283 }
284
285 $conditions = substr($c, 5);
286 }
287 else
288 {
289 $conditions_args = [];
290
291 if ($args)
292 {
293 if (is_array($args[0]))
294 {
295 $conditions_args = $args[0];
296 }
297 else
298 {
299
300
301
302
303 foreach ($args as $key => $value)
304 {
305 $conditions_args[$key] = $value;
306 }
307 }
308 }
309 }
310
311 foreach ($conditions_args as &$value)
312 {
313 if ($value instanceof \DateTime)
314 {
315 $value = DateTime::from($value)->utc->as_db;
316 }
317 }
318
319 return [ $conditions ? '(' . $conditions . ')' : null, $conditions_args ];
320 }
321
322 private function dynamic_filter($filter, array $conditions_args=[])
323 {
324 $conditions = explode('_and_', $filter);
325
326 return $this->where(array_combine($conditions, $conditions_args));
327 }
328
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
383 public function where($conditions, $conditions_args=null)
384 {
385 list($conditions, $conditions_args) = $this->defered_parse_conditions();
386
387 if ($conditions)
388 {
389 $this->conditions[] = $conditions;
390
391 if ($conditions_args)
392 {
393 $this->conditions_args = array_merge($this->conditions_args, $conditions_args);
394 }
395 }
396
397 return $this;
398 }
399
400 401 402 403 404 405 406
407 public function order($order_or_field_name, $field_values=null)
408 {
409 $this->order = func_get_args();
410
411 return $this;
412 }
413
414 415 416 417 418 419 420
421 public function group($group)
422 {
423 $this->group = $group;
424
425 return $this;
426 }
427
428 429 430 431 432 433 434 435
436 public function having($conditions, $conditions_args=null)
437 {
438 list($having, $having_args) = $this->defered_parse_conditions();
439
440 $this->having = $having;
441 $this->having_args = $having_args;
442
443 return $this;
444 }
445
446 447 448 449 450 451 452
453 public function offset($offset)
454 {
455 $this->offset = (int) $offset;
456
457 return $this;
458 }
459
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
479 public function limit($limit)
480 {
481 $offset = null;
482
483 if (func_num_args() == 2)
484 {
485 $offset = $limit;
486 $limit = func_get_arg(1);
487 }
488
489 $this->offset = (int) $offset;
490 $this->limit = (int) $limit;
491
492 return $this;
493 }
494
495 496 497 498 499 500 501 502 503
504 public function mode($mode)
505 {
506 $this->mode = func_get_args();
507
508 return $this;
509 }
510
511 512 513 514 515
516 protected function build()
517 {
518 $query = '';
519
520 if ($this->join)
521 {
522 $query .= ' ' . $this->join;
523 }
524
525 if ($this->conditions)
526 {
527 $query .= ' WHERE ' . implode(' AND ', $this->conditions);
528 }
529
530 if ($this->group)
531 {
532 $query .= ' GROUP BY ' . $this->group;
533
534 if ($this->having)
535 {
536 $query .= ' HAVING ' . $this->having;
537 }
538 }
539
540 $order = $this->order;
541
542 if ($order)
543 {
544 $query .= ' ' . $this->render_order($order);
545 }
546
547 $offset = $this->offset;
548 $limit = $this->limit;
549
550 if ($offset || $limit)
551 {
552 $query .= ' ' . $this->render_offset_and_limit($offset, $limit);
553 }
554
555 return $query;
556 }
557
558 protected function render_order($order)
559 {
560 if (count($order) == 1)
561 {
562 return 'ORDER BY ' . $order[0];
563 }
564
565 $connection = $this->model->connection;
566
567 $field = array_shift($order);
568 $field_values = is_array($order[0]) ? $order[0] : $order;
569 $field_values = array_map(function($v) use($connection) {
570
571 return $connection->quote($v);
572
573 }, $field_values);
574
575 return "ORDER BY FIELD($field, " . implode(', ', $field_values) . ")";
576 }
577
578 protected function render_offset_and_limit($offset, $limit)
579 {
580 if ($offset && $limit)
581 {
582 return "LIMIT $offset, $limit";
583 }
584 else if ($offset)
585 {
586 return "LIMIT $offset, " . self::LIMIT_MAX;
587 }
588 else if ($limit)
589 {
590 return "LIMIT $limit";
591 }
592 }
593
594 595 596 597 598 599 600 601
602 protected function prepare()
603 {
604 return $this->model->connection->prepare((string) $this);
605 }
606
607 608 609 610 611
612 public function query()
613 {
614 $statement = $this->prepare();
615 $statement->execute(array_merge($this->conditions_args, $this->having_args));
616
617 return $statement;
618 }
619
620 621 622 623 624
625 protected function get_prepared()
626 {
627 return $this->prepare();
628 }
629
630 631 632
633
634 private function resolve_fetch_mode()
635 {
636 $trace = debug_backtrace(false);
637
638 if ($trace[1]['args'])
639 {
640 $args = $trace[1]['args'];
641 }
642 else if ($this->mode)
643 {
644 $args = $this->mode;
645 }
646 else if ($this->select)
647 {
648 $args = [ \PDO::FETCH_ASSOC ];
649 }
650 else if ($this->model->activerecord_class)
651 {
652 $args = [ \PDO::FETCH_CLASS, $this->model->activerecord_class, [ $this->model ]];
653 }
654 else
655 {
656 $args = [ \PDO::FETCH_CLASS, 'ICanBoogie\ActiveRecord', [ $this->model ]];
657 }
658
659 return $args;
660 }
661
662 663 664 665 666
667 public function all()
668 {
669 $statement = $this->query();
670 $args = $this->resolve_fetch_mode();
671
672 return call_user_func_array([ $statement, 'fetchAll' ], $args);
673 }
674
675 676 677 678 679
680 protected function get_all()
681 {
682 return $this->all();
683 }
684
685 686 687 688 689 690
691 public function one()
692 {
693 $previous_limit = $this->limit;
694
695 $this->limit = 1;
696
697 $statement = $this->query();
698
699 $this->limit = $previous_limit;
700
701 $args = $this->resolve_fetch_mode();
702
703 if (count($args) > 1 && $args[0] == \PDO::FETCH_CLASS)
704 {
705 array_shift($args);
706
707 $rc = call_user_func_array([ $statement, 'fetchObject' ], $args);
708
709 $statement->closeCursor();
710
711 return $rc;
712 }
713
714 return call_user_func_array([ $statement, 'fetchAndClose' ], $args);
715 }
716
717 718 719 720 721 722 723
724 protected function get_one()
725 {
726 return $this->one();
727 }
728
729 730 731 732 733 734
735 protected function get_pairs()
736 {
737 return $this->all(\PDO::FETCH_KEY_PAIR);
738 }
739
740 741 742 743 744
745 protected function get_rc()
746 {
747 $previous_limit = $this->limit;
748
749 $this->limit = 1;
750
751 $statement = $this->query();
752
753 $this->limit = $previous_limit;
754
755 return $statement->fetchColumnAndClose();
756 }
757
758 759 760 761 762 763 764 765 766 767 768 769 770
771 public function exists($key=null)
772 {
773 $suffix = '';
774
775 if ($key !== null)
776 {
777 if (func_num_args() > 1)
778 {
779 $key = func_get_args();
780 }
781
782 $this->where([ '{primary}' => $key ]);
783 }
784 else if (!$this->limit)
785 {
786 $suffix = ' LIMIT 1';
787 }
788
789 $rc = $this
790 ->model
791 ->query('SELECT `{primary}` FROM {self_and_related}' . $this->build() . $suffix, array_merge($this->conditions_args, $this->having_args))
792 ->fetchAll(\PDO::FETCH_COLUMN);
793
794 if ($rc && is_array($key))
795 {
796 $exists = array_combine($key, array_fill(0, count($key), false));
797
798 foreach ($rc as $key)
799 {
800 $exists[$key] = true;
801 }
802
803 foreach ($exists as $v)
804 {
805 if (!$v)
806 {
807 return $exists;
808 }
809 }
810
811
812
813 return true;
814 }
815 else
816 {
817 $rc = !empty($rc);
818 }
819
820 return $rc;
821 }
822
823 824 825 826 827 828 829
830 protected function get_exists()
831 {
832 return $this->exists();
833 }
834
835 836 837 838 839 840 841 842
843 private function compute($method, $column)
844 {
845 $query = 'SELECT ';
846
847 if ($column)
848 {
849 if ($method == 'COUNT')
850 {
851 $query .= "`$column`, $method(`$column`)";
852
853 $this->group($column);
854 }
855 else
856 {
857 $query .= "$method(`$column`)";
858 }
859 }
860 else
861 {
862 $query .= $method . '(*)';
863 }
864
865 $query .= ' AS count FROM {self_and_related}' . $this->build();
866 $query = $this->model->query($query, array_merge($this->conditions_args, $this->having_args));
867
868 if ($method == 'COUNT' && $column)
869 {
870 return $query->fetchAll(\PDO::FETCH_KEY_PAIR);
871 }
872
873 return (int) $query->fetchColumnAndClose();
874 }
875
876 877 878 879 880
881 public function count($column=null)
882 {
883 return $this->compute('COUNT', $column);
884 }
885
886 887 888 889 890
891 protected function get_count()
892 {
893 return $this->count();
894 }
895
896 897 898 899 900 901 902
903 public function average($column)
904 {
905 return $this->compute('AVG', $column);
906 }
907
908 909 910 911 912 913 914
915 public function minimum($column)
916 {
917 return $this->compute('MIN', $column);
918 }
919
920 921 922 923 924 925 926
927 public function maximum($column)
928 {
929 return $this->compute('MAX', $column);
930 }
931
932 933 934 935 936 937 938
939 public function sum($column)
940 {
941 return $this->compute('SUM', $column);
942 }
943
944 945 946 947 948
949 public function delete()
950 {
951 $query = 'DELETE FROM {self} ' . $this->build();
952
953 return $this->model->execute($query, $this->conditions_args);
954 }
955
956 957 958
959 public function getIterator()
960 {
961 return new \ArrayIterator($this->all());
962 }
963 }