PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Database / Query / Builder.php

Builder.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at vendor/wpfluent/framework/src/WPFluent/Database/Query/Builder.php

4,554 lines 125.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Database\Query;
4
5 use Closure;
6 use DatePeriod;
7 use LogicException;
8 use RuntimeException;
9 use DateTimeInterface;
10 use InvalidArgumentException;
11 use FluentBoards\Framework\Foundation\App;
12 use FluentBoards\Framework\Support\Arr;
13 use FluentBoards\Framework\Support\Str;
14 use FluentBoards\Framework\Support\Helper;
15 use FluentBoards\Framework\Support\MacroableTrait;
16 use FluentBoards\Framework\Support\Collection;
17 use FluentBoards\Framework\Pagination\Paginator;
18 use FluentBoards\Framework\Support\ForwardsCalls;
19 use FluentBoards\Framework\Support\LazyCollection;
20 use FluentBoards\Framework\Database\Query\Expression;
21 use FluentBoards\Framework\Database\Query\Grammars\Grammar;
22 use FluentBoards\Framework\Database\Query\Processors\Processor;
23 use FluentBoards\Framework\Database\Query\ConditionExpression;
24 use FluentBoards\Framework\Support\ArrayableInterface;
25 use FluentBoards\Framework\Database\ConnectionInterface;
26 use FluentBoards\Framework\Database\Concerns\BuildsQueries;
27 use FluentBoards\Framework\Database\Concerns\BuildsWhereDateClauses;
28 use FluentBoards\Framework\Database\Concerns\ExplainsQueries;
29 use FluentBoards\Framework\Database\Orm\Relations\Relation;
30 use FluentBoards\Framework\Database\Orm\Builder as OrmBuilder;
31
32 class Builder
33 {
34 use BuildsQueries, BuildsWhereDateClauses, ExplainsQueries, ForwardsCalls, MacroableTrait {
35 __call as macroCall;
36 }
37
38 /**
39 * The database connection instance.
40 *
41 * @var \FluentBoards\Framework\Database\ConnectionInterface
42 */
43 public $connection;
44
45 /**
46 * The database query grammar instance.
47 *
48 * @var \FluentBoards\Framework\Database\Query\Grammars\Grammar
49 */
50 public $grammar;
51
52 /**
53 * The database query post processor instance.
54 *
55 * @var \FluentBoards\Framework\Database\Query\Processors\Processor
56 */
57 public $processor;
58
59 /**
60 * The current query value bindings.
61 *
62 * @var array
63 */
64 public $bindings = [
65 'select' => [],
66 'from' => [],
67 'join' => [],
68 'where' => [],
69 'groupBy' => [],
70 'having' => [],
71 'order' => [],
72 'union' => [],
73 'unionOrder' => [],
74 ];
75
76 /**
77 * An aggregate function and column to be run.
78 *
79 * @var array
80 */
81 public $aggregate;
82
83 /**
84 * The columns that should be returned.
85 *
86 * @var array
87 */
88 public $columns;
89
90 /**
91 * Indicates if the query returns distinct results.
92 *
93 * Occasionally contains the columns that should be distinct.
94 *
95 * @var bool|array
96 */
97 public $distinct = false;
98
99 /**
100 * The table which the query is targeting.
101 *
102 * @var string
103 */
104 public $from;
105
106 /**
107 * The index hint for the query.
108 *
109 * @var \FluentBoards\Framework\Database\Query\IndexHint
110 */
111 public $indexHint;
112
113 /**
114 * The table joins for the query.
115 *
116 * @var array
117 */
118 public $joins;
119
120 /**
121 * The where constraints for the query.
122 *
123 * @var array
124 */
125 public $wheres = [];
126
127 /**
128 * The groupings for the query.
129 *
130 * @var array
131 */
132 public $groups;
133
134 /**
135 * The having constraints for the query.
136 *
137 * @var array
138 */
139 public $havings;
140
141 /**
142 * The orderings for the query.
143 *
144 * @var array
145 */
146 public $orders;
147
148 /**
149 * The maximum number of records to return.
150 *
151 * @var int
152 */
153 public $limit;
154
155 /**
156 * The maximum number of records to return per group.
157 *
158 * @var array
159 */
160 public $groupLimit;
161
162 /**
163 * The number of records to skip.
164 *
165 * @var int
166 */
167 public $offset;
168
169 /**
170 * The query union statements.
171 *
172 * @var array
173 */
174 public $unions;
175
176 /**
177 * The maximum number of union records to return.
178 *
179 * @var int
180 */
181 public $unionLimit;
182
183 /**
184 * The number of union records to skip.
185 *
186 * @var int
187 */
188 public $unionOffset;
189
190 /**
191 * The orderings for the union query.
192 *
193 * @var array
194 */
195 public $unionOrders;
196
197 /**
198 * Indicates whether row locking is being used.
199 *
200 * @var string|bool
201 */
202 public $lock;
203
204 /**
205 * The callbacks that should be invoked before the query is executed.
206 *
207 * @var array
208 */
209 public $beforeQueryCallbacks = [];
210
211 /**
212 * The callbacks that should be invoked after retrieving data from the database.
213 *
214 * @var array
215 */
216 protected $afterQueryCallbacks = [];
217
218 /**
219 * All of the available clause operators.
220 *
221 * @var string[]
222 */
223 public $operators = [
224 '=', '<', '>', '<=', '>=', '<>', '!=', '<=>',
225 'like', 'like binary', 'not like', 'ilike',
226 '&', '|', '^', '<<', '>>', '&~', 'is', 'is not',
227 'rlike', 'not rlike', 'regexp', 'not regexp',
228 '~', '~*', '!~', '!~*', 'similar to',
229 'not similar to', 'not ilike', '~~*', '!~~*',
230 ];
231
232 /**
233 * All of the available bitwise operators.
234 *
235 * @var string[]
236 */
237 public $bitwiseOperators = [
238 '&', '|', '^', '<<', '>>', '&~',
239 ];
240
241 /**
242 * Whether to use write pdo for the select.
243 *
244 * @var bool
245 */
246 public $useWritePdo = false;
247
248 /**
249 * Allow dynamic property injection.
250 *
251 * @var array
252 */
253 protected $dynamicProperties = [];
254
255 /**
256 * Create a new query builder instance.
257 *
258 * @param \FluentBoards\Framework\Database\ConnectionInterface $connection
259 * @param \FluentBoards\Framework\Database\Query\Grammars\Grammar|null $grammar
260 * @param \FluentBoards\Framework\Database\Query\Processors\Processor|null $processor
261 * @return void
262 */
263 public function __construct(
264 ConnectionInterface $connection,
265 ?Grammar $grammar = null,
266 ?Processor $processor = null
267 ) {
268 $this->connection = $connection;
269 $this->grammar = $grammar ?: $connection->getQueryGrammar();
270 $this->processor = $processor ?: $connection->getPostProcessor();
271 }
272
273 /**
274 * Set the columns to be selected.
275 *
276 * @param array|mixed $columns
277 * @return $this
278 */
279 public function select($columns = ['*'])
280 {
281 $this->columns = [];
282 $this->bindings['select'] = [];
283
284 $columns = is_array($columns) ? $columns : func_get_args();
285
286 foreach ($columns as $as => $column) {
287 if (is_string($as) && $this->isQueryable($column)) {
288 $this->selectSub($column, $as);
289 } else {
290 $this->columns[] = $column;
291 }
292 }
293
294 return $this;
295 }
296
297 /**
298 * Add a subselect expression to the query.
299 *
300 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
301 * @param string $as
302 * @return $this
303 *
304 * @throws \InvalidArgumentException
305 */
306 public function selectSub($query, $as)
307 {
308 $this->grammar->addAlias($as);
309
310 [$query, $bindings] = $this->createSub($query);
311
312 return $this->selectRaw(
313 '('.$query.') as '.$this->grammar->wrap($as), $bindings
314 );
315 }
316
317 /**
318 * Add a new "raw" select expression to the query.
319 *
320 * @param string $expression
321 * @param array $bindings
322 * @return $this
323 */
324 public function selectRaw($expression, array $bindings = [])
325 {
326 $this->addSelect(new Expression($expression));
327
328 if ($bindings) {
329 $this->addBinding($bindings, 'select');
330 }
331
332 return $this;
333 }
334
335 /**
336 * Makes "from" fetch from a subquery.
337 *
338 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|string $query
339 * @param string $as
340 * @return $this
341 *
342 * @throws \InvalidArgumentException
343 */
344 public function fromSub($query, $as)
345 {
346 $this->grammar->addAlias($as);
347
348 [$query, $bindings] = $this->createSub($query);
349
350 return $this->fromRaw('('.$query.') as '.$this->grammar->wrapTable($as), $bindings);
351 }
352
353 /**
354 * Add a raw from clause to the query.
355 *
356 * @param string $expression
357 * @param mixed $bindings
358 * @return $this
359 */
360 public function fromRaw($expression, $bindings = [])
361 {
362 $this->from = new Expression($expression);
363
364 $this->addBinding($bindings, 'from');
365
366 return $this;
367 }
368
369 /**
370 * Creates a subquery and parse it.
371 *
372 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|string $query
373 * @return array
374 */
375 protected function createSub($query)
376 {
377 // If the given query is a Closure, we will execute it while passing in a new
378 // query instance to the Closure. This will give the developer a chance to
379 // format and work with the query before we cast it to a raw SQL string.
380 if ($query instanceof Closure) {
381 $callback = $query;
382
383 $callback($query = $this->forSubQuery());
384 }
385
386 return $this->parseSub($query);
387 }
388
389 /**
390 * Parse the subquery into SQL and bindings.
391 *
392 * @param mixed $query
393 * @return array
394 *
395 * @throws \InvalidArgumentException
396 */
397 protected function parseSub($query)
398 {
399 if ($query instanceof self || $query instanceof OrmBuilder || $query instanceof Relation) {
400 $query = $this->prependDatabaseNameIfCrossDatabaseQuery($query);
401
402 return [$query->toSql(), $query->getBindings()];
403 } elseif (is_string($query)) {
404 return [$query, []];
405 } else {
406 throw new InvalidArgumentException(
407 'A subquery must be a query builder instance, a Closure, or a string.'
408 );
409 }
410 }
411
412 /**
413 * Prepend the database name if the given query is on another database.
414 *
415 * @param mixed $query
416 * @return mixed
417 */
418 protected function prependDatabaseNameIfCrossDatabaseQuery($query)
419 {
420 if ($query->getConnection()->getDatabaseName() !==
421 $this->getConnection()->getDatabaseName()) {
422 $databaseName = $query->getConnection()->getDatabaseName();
423
424 if (! str_starts_with($query->from, $databaseName) && ! str_contains($query->from, '.')) {
425 $query->from($databaseName.'.'.$query->from);
426 }
427 }
428
429 return $query;
430 }
431
432 /**
433 * Add a new select column to the query.
434 *
435 * @param array|mixed $column
436 * @return $this
437 */
438 public function addSelect($column)
439 {
440 $columns = is_array($column) ? $column : func_get_args();
441
442 foreach ($columns as $as => $column) {
443 if (is_string($as) && $this->isQueryable($column)) {
444 if (is_null($this->columns)) {
445 $this->select($this->from.'.*');
446 }
447
448 $this->selectSub($column, $as);
449 } else {
450 if (is_array($this->columns) && in_array($column, $this->columns, true)) {
451 continue;
452 }
453
454 $this->columns[] = $column;
455 }
456 }
457
458 return $this;
459 }
460
461 /**
462 * Force the query to only return distinct results.
463 *
464 * @return $this
465 */
466 public function distinct()
467 {
468 $columns = func_get_args();
469
470 if (count($columns) > 0) {
471 $this->distinct = is_array($columns[0]) || is_bool($columns[0]) ? $columns[0] : $columns;
472 } else {
473 $this->distinct = true;
474 }
475
476 return $this;
477 }
478
479 /**
480 * Set the table which the query is targeting.
481 *
482 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|string $table
483 * @param string|null $as
484 * @return $this
485 */
486 public function from($table, $as = null)
487 {
488 if (is_string($table) && stripos($table, ' as ') !== false) {
489 [$table, $as] = explode(' as ', $table);
490 $this->grammar->addAlias($as);
491 }
492
493 if ($this->isQueryable($table)) {
494 return $this->fromSub($table, $as);
495 }
496
497 $this->from = $as ? "{$table} as {$as}" : $table;
498
499 return $this;
500 }
501
502 /**
503 * Add an index hint to suggest a query index.
504 *
505 * @param string $index
506 * @return $this
507 */
508 public function useIndex($index)
509 {
510 $this->indexHint = new IndexHint('hint', $index);
511
512 return $this;
513 }
514
515 /**
516 * Add an index hint to force a query index.
517 *
518 * @param string $index
519 * @return $this
520 */
521 public function forceIndex($index)
522 {
523 $this->indexHint = new IndexHint('force', $index);
524
525 return $this;
526 }
527
528 /**
529 * Add an index hint to ignore a query index.
530 *
531 * @param string $index
532 * @return $this
533 */
534 public function ignoreIndex($index)
535 {
536 $this->indexHint = new IndexHint('ignore', $index);
537
538 return $this;
539 }
540
541 /**
542 * Add a join clause to the query.
543 *
544 * @param string $table
545 * @param \Closure|string $first
546 * @param string|null $operator
547 * @param string|null $second
548 * @param string $type
549 * @param bool $where
550 * @return $this
551 */
552 public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
553 {
554 $join = $this->newJoinClause($this, $type, $table);
555
556 // If the first "column" of the join is really a Closure instance the developer
557 // is trying to build a join with a complex "on" clause containing more than
558 // one condition, so we'll add the join and call a Closure with the query.
559 if ($first instanceof Closure) {
560 $first($join);
561
562 $this->joins[] = $join;
563
564 $this->addBinding($join->getBindings(), 'join');
565 }
566
567 // If the column is simply a string, we can assume the join simply has a basic
568 // "on" clause with a single condition. So we will just build the join with
569 // this simple join clauses attached to it. There is not a join callback.
570 else {
571 $method = $where ? 'where' : 'on';
572
573 $this->joins[] = $join->$method($first, $operator, $second);
574
575 $this->addBinding($join->getBindings(), 'join');
576 }
577
578 return $this;
579 }
580
581 /**
582 * Add a "join where" clause to the query.
583 *
584 * @param string $table
585 * @param \Closure|string $first
586 * @param string $operator
587 * @param string $second
588 * @param string $type
589 * @return $this
590 */
591 public function joinWhere($table, $first, $operator, $second, $type = 'inner')
592 {
593 return $this->join($table, $first, $operator, $second, $type, true);
594 }
595
596 /**
597 * Add a subquery join clause to the query.
598 *
599 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
600 * @param string $as
601 * @param \Closure|string $first
602 * @param string|null $operator
603 * @param string|null $second
604 * @param string $type
605 * @param bool $where
606 * @return $this
607 *
608 * @throws \InvalidArgumentException
609 */
610 public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)
611 {
612 $this->grammar->addAlias($as);
613
614 [$query, $bindings] = $this->createSub($query);
615
616 $expression = '('.$query.') as '.$this->grammar->wrapTable($as);
617
618 $this->addBinding($bindings, 'join');
619
620 return $this->join(new Expression($expression), $first, $operator, $second, $type, $where);
621 }
622
623 /**
624 * Add a lateral join clause to the query.
625 *
626 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
627 * @param string $as
628 * @param string $type
629 * @return $this
630 */
631 public function joinLateral($query, string $as, string $type = 'inner')
632 {
633 [$query, $bindings] = $this->createSub($query);
634
635 $expression = '('.$query.') as '.$this->grammar->wrapTable($as);
636
637 $this->addBinding($bindings, 'join');
638
639 $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression));
640
641 return $this;
642 }
643
644 /**
645 * Add a lateral left join to the query.
646 *
647 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
648 * @param string $as
649 * @return $this
650 */
651 public function leftJoinLateral($query, string $as)
652 {
653 return $this->joinLateral($query, $as, 'left');
654 }
655
656 /**
657 * Add a left join to the query.
658 *
659 * @param string $table
660 * @param \Closure|string $first
661 * @param string|null $operator
662 * @param string|null $second
663 * @return $this
664 */
665 public function leftJoin($table, $first, $operator = null, $second = null)
666 {
667 return $this->join($table, $first, $operator, $second, 'left');
668 }
669
670 /**
671 * Add a "join where" clause to the query.
672 *
673 * @param string $table
674 * @param \Closure|string $first
675 * @param string $operator
676 * @param string $second
677 * @return $this
678 */
679 public function leftJoinWhere($table, $first, $operator, $second)
680 {
681 return $this->joinWhere($table, $first, $operator, $second, 'left');
682 }
683
684 /**
685 * Add a subquery left join to the query.
686 *
687 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
688 * @param string $as
689 * @param \Closure|string $first
690 * @param string|null $operator
691 * @param string|null $second
692 * @return $this
693 */
694 public function leftJoinSub($query, $as, $first, $operator = null, $second = null)
695 {
696 return $this->joinSub($query, $as, $first, $operator, $second, 'left');
697 }
698
699 /**
700 * Add a right join to the query.
701 *
702 * @param string $table
703 * @param \Closure|string $first
704 * @param string|null $operator
705 * @param string|null $second
706 * @return $this
707 */
708 public function rightJoin($table, $first, $operator = null, $second = null)
709 {
710 return $this->join($table, $first, $operator, $second, 'right');
711 }
712
713 /**
714 * Add a "right join where" clause to the query.
715 *
716 * @param string $table
717 * @param \Closure|string $first
718 * @param string $operator
719 * @param string $second
720 * @return $this
721 */
722 public function rightJoinWhere($table, $first, $operator, $second)
723 {
724 return $this->joinWhere($table, $first, $operator, $second, 'right');
725 }
726
727 /**
728 * Add a subquery right join to the query.
729 *
730 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
731 * @param string $as
732 * @param \Closure|string $first
733 * @param string|null $operator
734 * @param string|null $second
735 * @return $this
736 */
737 public function rightJoinSub($query, $as, $first, $operator = null, $second = null)
738 {
739 return $this->joinSub($query, $as, $first, $operator, $second, 'right');
740 }
741
742 /**
743 * Add a "cross join" clause to the query.
744 *
745 * @param string $table
746 * @param \Closure|string|null $first
747 * @param string|null $operator
748 * @param string|null $second
749 * @return $this
750 */
751 public function crossJoin($table, $first = null, $operator = null, $second = null)
752 {
753 if ($first) {
754 return $this->join($table, $first, $operator, $second, 'cross');
755 }
756
757 $this->joins[] = $this->newJoinClause($this, 'cross', $table);
758
759 return $this;
760 }
761
762 /**
763 * Add a subquery cross join to the query.
764 *
765 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|string $query
766 * @param string $as
767 * @return $this
768 */
769 public function crossJoinSub($query, $as)
770 {
771 $this->grammar->addAlias($as);
772
773 [$query, $bindings] = $this->createSub($query);
774
775 $expression = '('.$query.') as '.$this->grammar->wrapTable($as);
776
777 $this->addBinding($bindings, 'join');
778
779 $this->joins[] = $this->newJoinClause($this, 'cross', new Expression($expression));
780
781 return $this;
782 }
783
784 /**
785 * Get a new join clause.
786 *
787 * @param \FluentBoards\Framework\Database\Query\Builder $parentQuery
788 * @param string $type
789 * @param string $table
790 * @return \FluentBoards\Framework\Database\Query\JoinClause
791 */
792 protected function newJoinClause(self $parentQuery, $type, $table)
793 {
794 if (stripos($table, ' as ') !== false) {
795 [$_, $as] = explode(' as ', $table);
796 $this->grammar->addAlias($as);
797 }
798
799 return new JoinClause($parentQuery, $type, $table);
800 }
801
802 /**
803 * Get a new join lateral clause.
804 *
805 * @param \FluentBoards\Framework\Database\Query\Builder $parentQuery
806 * @param string $type
807 * @param string $table
808 * @return \FluentBoards\Framework\Database\Query\JoinLateralClause
809 */
810 protected function newJoinLateralClause(self $parentQuery, $type, $table)
811 {
812 return new JoinLateralClause($parentQuery, $type, $table);
813 }
814
815 /**
816 * Merge an array of where clauses and bindings.
817 *
818 * @param array $wheres
819 * @param array $bindings
820 * @return $this
821 */
822 public function mergeWheres($wheres, $bindings)
823 {
824 $this->wheres = array_merge($this->wheres, (array) $wheres);
825
826 $this->bindings['where'] = array_values(
827 array_merge($this->bindings['where'], (array) $bindings)
828 );
829
830 return $this;
831 }
832
833 /**
834 * Add a basic where clause to the query.
835 *
836 * @param \Closure|string|array $column
837 * @param mixed $operator
838 * @param mixed $value
839 * @param string $boolean
840 * @return $this
841 */
842 public function where($column, $operator = null, $value = null, $boolean = 'and')
843 {
844 if ($column instanceof ConditionExpression) {
845 $type = 'Expression';
846
847 $this->wheres[] = compact('type', 'column', 'boolean');
848
849 return $this;
850 }
851
852 // If the column is an array, we will assume it is an array of key-value pairs
853 // and can add them each as a where clause. We will maintain the boolean we
854 // received when the method was called and pass it into the nested where.
855 if (is_array($column)) {
856 return $this->addArrayOfWheres($column, $boolean);
857 }
858
859 // Here we will make some assumptions about the operator. If only 2 values are
860 // passed to the method, we will assume that the operator is an equals sign
861 // and keep going. Otherwise, we'll require the operator to be passed in.
862 [$value, $operator] = $this->prepareValueAndOperator(
863 $value, $operator, func_num_args() === 2
864 );
865
866 // If the column is actually a Closure instance, we will assume the developer
867 // wants to begin a nested where statement which is wrapped in parentheses.
868 // We will add that Closure to the query and return back out immediately.
869 if ($column instanceof Closure && is_null($operator)) {
870 return $this->whereNested($column, $boolean);
871 }
872
873 // If the column is a Closure instance and there is an operator value, we will
874 // assume the developer wants to run a subquery and then compare the result
875 // of that subquery with the given value that was provided to the method.
876 if ($this->isQueryable($column) && ! is_null($operator)) {
877 [$sub, $bindings] = $this->createSub($column);
878
879 return $this->addBinding($bindings, 'where')
880 ->where(new Expression('('.$sub.')'), $operator, $value, $boolean);
881 }
882
883 // If the given operator is not found in the list of valid operators we will
884 // assume that the developer is just short-cutting the '=' operators and
885 // we will set the operators to '=' and set the values appropriately.
886 if ($this->invalidOperator($operator)) {
887 [$value, $operator] = [$operator, '='];
888 }
889
890 // If the value is a Closure, it means the developer is performing an entire
891 // sub-select within the query and we will need to compile the sub-select
892 // within the where clause to get the appropriate query record results.
893 if ($this->isQueryable($value)) {
894 return $this->whereSub($column, $operator, $value, $boolean);
895 }
896
897 // If the value is "null", we will just assume the developer wants to add a
898 // where null clause to the query. So, we will allow a short-cut here to
899 // that method for convenience so the developer doesn't have to check.
900 if (is_null($value)) {
901 return $this->whereNull($column, $boolean, $operator !== '=');
902 }
903
904 $type = 'Basic';
905
906 $columnString = ($column instanceof Expression)
907 ? $this->grammar->getValue($column)
908 : $column;
909
910 // If the column is making a JSON reference we'll check to see if the value
911 // is a boolean. If it is, we'll add the raw boolean string as an actual
912 // value to the query to ensure this is properly handled by the query.
913 if (str_contains($columnString, '->') && is_bool($value)) {
914 $value = new Expression($value ? 'true' : 'false');
915
916 if (is_string($column)) {
917 $type = 'JsonBoolean';
918 }
919 }
920
921 if ($this->isBitwiseOperator($operator)) {
922 $type = 'Bitwise';
923 }
924
925 // Now that we are working with just a simple query we can put the elements
926 // in our array and add the query binding to our array of bindings that
927 // will be bound to each SQL statements when it is finally executed.
928 $this->wheres[] = compact(
929 'type', 'column', 'operator', 'value', 'boolean'
930 );
931
932 if (! $value instanceof Expression) {
933 $this->addBinding($this->flattenValue($value), 'where');
934 }
935
936 return $this;
937 }
938
939 /**
940 * Add an array of where clauses to the query.
941 *
942 * @param array $column
943 * @param string $boolean
944 * @param string $method
945 * @return $this
946 */
947 protected function addArrayOfWheres($column, $boolean, $method = 'where')
948 {
949 return $this->whereNested(function ($query) use ($column, $method, $boolean) {
950 foreach ($column as $key => $value) {
951 if (is_numeric($key) && is_array($value)) {
952 $query->{$method}(...array_values($value));
953 } else {
954 $query->{$method}($key, '=', $value, $boolean);
955 }
956 }
957 }, $boolean);
958 }
959
960 /**
961 * Prepare the value and operator for a where clause.
962 *
963 * @param string $value
964 * @param string $operator
965 * @param bool $useDefault
966 * @return array
967 *
968 * @throws \InvalidArgumentException
969 */
970 public function prepareValueAndOperator($value, $operator, $useDefault = false)
971 {
972 if ($useDefault) {
973 return [$operator, '='];
974 } elseif ($this->invalidOperatorAndValue($operator, $value)) {
975 throw new InvalidArgumentException(
976 'Illegal operator and value combination.'
977 );
978 }
979
980 return [$value, $operator];
981 }
982
983 /**
984 * Determine if the given operator and value combination is legal.
985 *
986 * Prevents using Null values with invalid operators.
987 *
988 * @param string $operator
989 * @param mixed $value
990 * @return bool
991 */
992 protected function invalidOperatorAndValue($operator, $value)
993 {
994 return is_null($value) && in_array($operator, $this->operators) &&
995 ! in_array($operator, ['=', '<>', '!=']);
996 }
997
998 /**
999 * Determine if the given operator is supported.
1000 *
1001 * @param string $operator
1002 * @return bool
1003 */
1004 protected function invalidOperator($operator)
1005 {
1006 return ! is_string($operator) || (! in_array(strtolower($operator), $this->operators, true) &&
1007 ! in_array(strtolower($operator), $this->grammar->getOperators(), true));
1008 }
1009
1010 /**
1011 * Determine if the operator is a bitwise operator.
1012 *
1013 * @param string $operator
1014 * @return bool
1015 */
1016 protected function isBitwiseOperator($operator)
1017 {
1018 return in_array(strtolower($operator), $this->bitwiseOperators, true) ||
1019 in_array(strtolower($operator), $this->grammar->getBitwiseOperators(), true);
1020 }
1021
1022 /**
1023 * Add an "or where" clause to the query.
1024 *
1025 * @param \Closure|string|array $column
1026 * @param mixed $operator
1027 * @param mixed $value
1028 * @return $this
1029 */
1030 public function orWhere($column, $operator = null, $value = null)
1031 {
1032 [$value, $operator] = $this->prepareValueAndOperator(
1033 $value, $operator, func_num_args() === 2
1034 );
1035
1036 return $this->where($column, $operator, $value, 'or');
1037 }
1038
1039 /**
1040 * Add a basic "where not" clause to the query.
1041 *
1042 * @param \Closure|string|array|\FluentBoards\Framework\Database\Query\Expression $column
1043 * @param mixed $operator
1044 * @param mixed $value
1045 * @param string $boolean
1046 * @return $this
1047 */
1048 public function whereNot(
1049 $column,
1050 $operator = null,
1051 $value = null,
1052 $boolean = 'and'
1053 ) {
1054 if (is_array($column)) {
1055 return $this->whereNested(function ($query) use ($column, $operator, $value, $boolean) {
1056 $query->where($column, $operator, $value, $boolean);
1057 }, $boolean.' not');
1058 }
1059
1060 return $this->where($column, $operator, $value, $boolean.' not');
1061 }
1062
1063 /**
1064 * Add an "or where not" clause to the query.
1065 *
1066 * @param \Closure|string|array|\FluentBoards\Framework\Database\Query\Expression $column
1067 * @param mixed $operator
1068 * @param mixed $value
1069 * @return $this
1070 */
1071 public function orWhereNot($column, $operator = null, $value = null)
1072 {
1073 return $this->whereNot($column, $operator, $value, 'or');
1074 }
1075
1076 /**
1077 * Add a "where" clause comparing two columns to the query.
1078 *
1079 * @param string|array $first
1080 * @param string|null $operator
1081 * @param string|null $second
1082 * @param string|null $boolean
1083 * @return $this
1084 */
1085 public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
1086 {
1087 // If the column is an array, we will assume it is an array of key-value pairs
1088 // and can add them each as a where clause. We will maintain the boolean we
1089 // received when the method was called and pass it into the nested where.
1090 if (is_array($first)) {
1091 return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
1092 }
1093
1094 // If the given operator is not found in the list of valid operators we will
1095 // assume that the developer is just short-cutting the '=' operators and
1096 // we will set the operators to '=' and set the values appropriately.
1097 if ($this->invalidOperator($operator)) {
1098 [$second, $operator] = [$operator, '='];
1099 }
1100
1101 // Finally, we will add this where clause into this array of clauses that we
1102 // are building for the query. All of them will be compiled via a grammar
1103 // once the query is about to be executed and run against the database.
1104 $type = 'Column';
1105
1106 $this->wheres[] = compact(
1107 'type', 'first', 'operator', 'second', 'boolean'
1108 );
1109
1110 return $this;
1111 }
1112
1113 /**
1114 * Add an "or where" clause comparing two columns to the query.
1115 *
1116 * @param string|array $first
1117 * @param string|null $operator
1118 * @param string|null $second
1119 * @return $this
1120 */
1121 public function orWhereColumn($first, $operator = null, $second = null)
1122 {
1123 return $this->whereColumn($first, $operator, $second, 'or');
1124 }
1125
1126 /**
1127 * Add a raw where clause to the query.
1128 *
1129 * @param string $sql
1130 * @param mixed $bindings
1131 * @param string $boolean
1132 * @return $this
1133 */
1134 public function whereRaw($sql, $bindings = [], $boolean = 'and')
1135 {
1136 $this->wheres[] = ['type' => 'raw', 'sql' => $sql, 'boolean' => $boolean];
1137
1138 $this->addBinding((array) $bindings, 'where');
1139
1140 return $this;
1141 }
1142
1143 /**
1144 * Add a raw or where clause to the query.
1145 *
1146 * @param string $sql
1147 * @param mixed $bindings
1148 * @return $this
1149 */
1150 public function orWhereRaw($sql, $bindings = [])
1151 {
1152 return $this->whereRaw($sql, $bindings, 'or');
1153 }
1154
1155 /**
1156 * Add a "where like" clause to the query.
1157 *
1158 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1159 * @param string $value
1160 * @param bool $caseSensitive
1161 * @param string $boolean
1162 * @param bool $not
1163 * @return $this
1164 */
1165 public function whereLike(
1166 $column,
1167 $value,
1168 $caseSensitive = false,
1169 $boolean = 'and',
1170 $not = false
1171 ) {
1172 $type = 'Like';
1173
1174 $this->wheres[] = compact(
1175 'type', 'column', 'value', 'caseSensitive', 'boolean', 'not'
1176 );
1177
1178 if (method_exists($this->grammar, 'prepareWhereLikeBinding')) {
1179 $value = $this->grammar->prepareWhereLikeBinding(
1180 $value, $caseSensitive
1181 );
1182 }
1183
1184 if (!str_contains($value, '%')) {
1185 $value = '%'.$value.'%';
1186 }
1187
1188 $this->addBinding($value);
1189
1190 return $this;
1191 }
1192
1193 /**
1194 * Add an "or where like" clause to the query.
1195 *
1196 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1197 * @param string $value
1198 * @param bool $caseSensitive
1199 * @return $this
1200 */
1201 public function orWhereLike($column, $value, $caseSensitive = false)
1202 {
1203 return $this->whereLike($column, $value, $caseSensitive, 'or', false);
1204 }
1205
1206 /**
1207 * Add a "where not like" clause to the query.
1208 *
1209 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1210 * @param string $value
1211 * @param bool $caseSensitive
1212 * @param string $boolean
1213 * @return $this
1214 */
1215 public function whereNotLike(
1216 $column,
1217 $value,
1218 $caseSensitive = false,
1219 $boolean = 'and'
1220 ) {
1221 return $this->whereLike($column, $value, $caseSensitive, $boolean, true);
1222 }
1223
1224 /**
1225 * Add an "or where not like" clause to the query.
1226 *
1227 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1228 * @param string $value
1229 * @param bool $caseSensitive
1230 * @return $this
1231 */
1232 public function orWhereNotLike($column, $value, $caseSensitive = false)
1233 {
1234 return $this->whereNotLike($column, $value, $caseSensitive, 'or');
1235 }
1236
1237 /**
1238 * Add a "where like" clause to the query.
1239 *
1240 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1241 * @param string $value
1242 * @param bool $caseSensitive
1243 * @param string $boolean
1244 * @param bool $not
1245 * @return $this
1246 */
1247 public function whereStartsLike(
1248 $column,
1249 $value,
1250 $caseSensitive = false,
1251 $boolean = 'and',
1252 $not = false
1253 ) {
1254 return $this->whereLike(
1255 $column, $value.'%', $caseSensitive, $boolean, $not
1256 );
1257 }
1258
1259 /**
1260 * Add a "where like" clause to the query.
1261 *
1262 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1263 * @param string $value
1264 * @param bool $caseSensitive
1265 * @param string $boolean
1266 * @param bool $not
1267 * @return $this
1268 */
1269 public function whereEndsLike(
1270 $column,
1271 $value,
1272 $caseSensitive = false,
1273 $boolean = 'and',
1274 $not = false
1275 ) {
1276 return $this->whereLike(
1277 $column, '%'.$value, $caseSensitive, $boolean, $not
1278 );
1279 }
1280
1281 /**
1282 * Add a "where in" clause to the query.
1283 *
1284 * @param string $column
1285 * @param mixed $values
1286 * @param string $boolean
1287 * @param bool $not
1288 * @return $this
1289 */
1290 public function whereIn($column, $values, $boolean = 'and', $not = false)
1291 {
1292 $type = $not ? 'NotIn' : 'In';
1293
1294 // If the value is a query builder instance we will assume the developer wants to
1295 // look for any values that exists within this given query. So we will add the
1296 // query accordingly so that this query is properly executed when it is run.
1297 if ($this->isQueryable($values)) {
1298 [$query, $bindings] = $this->createSub($values);
1299
1300 $values = [new Expression($query)];
1301
1302 $this->addBinding($bindings, 'where');
1303 }
1304
1305 // Next, if the value is ArrayableInterface we need to cast it to its raw
1306 // array form so we have the underlying array value instead of an
1307 // Arrayable object which is not able to be added as a binding,
1308 // etc. We will then add to the wheres array.
1309 if ($values instanceof ArrayableInterface) {
1310 $values = $values->toArray();
1311 }
1312
1313 $this->wheres[] = compact('type', 'column', 'values', 'boolean');
1314
1315 if (count($values) !== count(Arr::flatten($values, 1))) {
1316 throw new InvalidArgumentException('Nested arrays may not be passed to whereIn method.');
1317 }
1318
1319 // Finally, we'll add a binding for each value unless that value is an
1320 // expression in which case we will just skip over it since it will
1321 // be the query as a raw string and not as a parameterized
1322 // place-holder to be replaced by the PDO.
1323 $this->addBinding($this->cleanBindings($values), 'where');
1324
1325 return $this;
1326 }
1327
1328 /**
1329 * Add an "or where in" clause to the query.
1330 *
1331 * @param string $column
1332 * @param mixed $values
1333 * @return $this
1334 */
1335 public function orWhereIn($column, $values)
1336 {
1337 return $this->whereIn($column, $values, 'or');
1338 }
1339
1340 /**
1341 * Add a "where not in" clause to the query.
1342 *
1343 * @param string $column
1344 * @param mixed $values
1345 * @param string $boolean
1346 * @return $this
1347 */
1348 public function whereNotIn($column, $values, $boolean = 'and')
1349 {
1350 return $this->whereIn($column, $values, $boolean, true);
1351 }
1352
1353 /**
1354 * Add an "or where not in" clause to the query.
1355 *
1356 * @param string $column
1357 * @param mixed $values
1358 * @return $this
1359 */
1360 public function orWhereNotIn($column, $values)
1361 {
1362 return $this->whereNotIn($column, $values, 'or');
1363 }
1364
1365 /**
1366 * Add a "where in raw" clause for integer values to the query.
1367 *
1368 * @param string $column
1369 * @param \FluentBoards\Framework\Support\ArrayableInterface|array $values
1370 * @param string $boolean
1371 * @param bool $not
1372 * @return $this
1373 */
1374 public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
1375 {
1376 $type = $not ? 'NotInRaw' : 'InRaw';
1377
1378 if ($values instanceof ArrayableInterface) {
1379 $values = $values->toArray();
1380 }
1381
1382 $values = Arr::flatten($values);
1383
1384 foreach ($values as &$value) {
1385 if (class_exists('BackedEnum') && $value instanceof \BackedEnum) {
1386 $value = (int) $value->value;
1387 } else {
1388 $value = (int) $value;
1389 }
1390 }
1391
1392 $this->wheres[] = compact('type', 'column', 'values', 'boolean');
1393
1394 return $this;
1395 }
1396
1397 /**
1398 * Add an "or where in raw" clause for integer values to the query.
1399 *
1400 * @param string $column
1401 * @param \FluentBoards\Framework\Support\ArrayableInterface|array $values
1402 * @return $this
1403 */
1404 public function orWhereIntegerInRaw($column, $values)
1405 {
1406 return $this->whereIntegerInRaw($column, $values, 'or');
1407 }
1408
1409 /**
1410 * Add a "where not in raw" clause for integer values to the query.
1411 *
1412 * @param string $column
1413 * @param \FluentBoards\Framework\Support\ArrayableInterface|array $values
1414 * @param string $boolean
1415 * @return $this
1416 */
1417 public function whereIntegerNotInRaw($column, $values, $boolean = 'and')
1418 {
1419 return $this->whereIntegerInRaw($column, $values, $boolean, true);
1420 }
1421
1422 /**
1423 * Add an "or where not in raw" clause for integer values to the query.
1424 *
1425 * @param string $column
1426 * @param \FluentBoards\Framework\Support\ArrayableInterface|array $values
1427 * @return $this
1428 */
1429 public function orWhereIntegerNotInRaw($column, $values)
1430 {
1431 return $this->whereIntegerNotInRaw($column, $values, 'or');
1432 }
1433
1434 /**
1435 * Add a "where null" clause to the query.
1436 *
1437 * @param string|array $columns
1438 * @param string $boolean
1439 * @param bool $not
1440 * @return $this
1441 */
1442 public function whereNull($columns, $boolean = 'and', $not = false)
1443 {
1444 $type = $not ? 'NotNull' : 'Null';
1445
1446 foreach (Arr::wrap($columns) as $column) {
1447 $this->wheres[] = compact('type', 'column', 'boolean');
1448 }
1449
1450 return $this;
1451 }
1452
1453 /**
1454 * Add an "or where null" clause to the query.
1455 *
1456 * @param string|array $column
1457 * @return $this
1458 */
1459 public function orWhereNull($column)
1460 {
1461 return $this->whereNull($column, 'or');
1462 }
1463
1464 /**
1465 * Add a "where not null" clause to the query.
1466 *
1467 * @param string|array $columns
1468 * @param string $boolean
1469 * @return $this
1470 */
1471 public function whereNotNull($columns, $boolean = 'and')
1472 {
1473 return $this->whereNull($columns, $boolean, true);
1474 }
1475
1476 /**
1477 * Add a where between statement to the query.
1478 *
1479 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1480 * @param array $values
1481 * @param string $boolean
1482 * @param bool $not
1483 * @return $this
1484 */
1485 public function whereBetween($column, array $values, $boolean = 'and', $not = false)
1486 {
1487 $type = 'between';
1488
1489 $type = 'between';
1490
1491 if ($values instanceof DatePeriod) {
1492 $values = [$values->getStartDate(), $values->getEndDate()];
1493 }
1494
1495 $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not');
1496
1497 $this->addBinding(
1498 array_slice(
1499 $this->cleanBindings(Arr::flatten($values)), 0, 2
1500 ), 'where'
1501 );
1502
1503 return $this;
1504 }
1505
1506 /**
1507 * Add a where between statement using columns to the query.
1508 *
1509 * @param string $column
1510 * @param array $values
1511 * @param string $boolean
1512 * @param bool $not
1513 * @return $this
1514 */
1515 public function whereBetweenColumns(
1516 $column,
1517 array $values,
1518 $boolean = 'and',
1519 $not = false
1520 ) {
1521 $type = 'betweenColumns';
1522
1523 $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not');
1524
1525 return $this;
1526 }
1527
1528 /**
1529 * Add an or where between statement to the query.
1530 *
1531 * @param string $column
1532 * @param array $values
1533 * @return $this
1534 */
1535 public function orWhereBetween($column, array $values)
1536 {
1537 return $this->whereBetween($column, $values, 'or');
1538 }
1539
1540 /**
1541 * Add an or where between statement using columns to the query.
1542 *
1543 * @param string $column
1544 * @param array $values
1545 * @return $this
1546 */
1547 public function orWhereBetweenColumns($column, array $values)
1548 {
1549 return $this->whereBetweenColumns($column, $values, 'or');
1550 }
1551
1552 /**
1553 * Add a where not between statement to the query.
1554 *
1555 * @param string $column
1556 * @param array $values
1557 * @param string $boolean
1558 * @return $this
1559 */
1560 public function whereNotBetween($column, iterable $values, $boolean = 'and')
1561 {
1562 return $this->whereBetween($column, $values, $boolean, true);
1563 }
1564
1565 /**
1566 * Add a where not between statement using columns to the query.
1567 *
1568 * @param string $column
1569 * @param array $values
1570 * @param string $boolean
1571 * @return $this
1572 */
1573 public function whereNotBetweenColumns($column, array $values, $boolean = 'and')
1574 {
1575 return $this->whereBetweenColumns($column, $values, $boolean, true);
1576 }
1577
1578 /**
1579 * Add an or where not between statement to the query.
1580 *
1581 * @param string $column
1582 * @param array $values
1583 * @return $this
1584 */
1585 public function orWhereNotBetween($column, iterable $values)
1586 {
1587 return $this->whereNotBetween($column, $values, 'or');
1588 }
1589
1590 /**
1591 * Add an or where not between statement using columns to the query.
1592 *
1593 * @param string $column
1594 * @param array $values
1595 * @return $this
1596 */
1597 public function orWhereNotBetweenColumns($column, array $values)
1598 {
1599 return $this->whereNotBetweenColumns($column, $values, 'or');
1600 }
1601
1602 /**
1603 * Add an "or where not null" clause to the query.
1604 *
1605 * @param string $column
1606 * @return $this
1607 */
1608 public function orWhereNotNull($column)
1609 {
1610 return $this->whereNotNull($column, 'or');
1611 }
1612
1613 /**
1614 * Add a "where date" statement to the query.
1615 *
1616 * @param \FluentBoards\Framework\Database\Query\Expression|string $column
1617 * @param string $operator
1618 * @param \DateTimeInterface|string|null $value
1619 * @param string $boolean
1620 * @return $this
1621 */
1622 public function whereDate($column, $operator, $value = null, $boolean = 'and')
1623 {
1624 [$value, $operator] = $this->prepareValueAndOperator(
1625 $value, $operator, func_num_args() === 2
1626 );
1627
1628 $value = $this->flattenValue($value);
1629
1630 if ($value instanceof DateTimeInterface) {
1631 $value = $value->format('Y-m-d');
1632 }
1633
1634 return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean);
1635 }
1636
1637 /**
1638 * Add an "or where date" statement to the query.
1639 *
1640 * @param string $column
1641 * @param string $operator
1642 * @param \DateTimeInterface|string|null $value
1643 * @return $this
1644 */
1645 public function orWhereDate($column, $operator, $value = null)
1646 {
1647 [$value, $operator] = $this->prepareValueAndOperator(
1648 $value, $operator, func_num_args() === 2
1649 );
1650
1651 return $this->whereDate($column, $operator, $value, 'or');
1652 }
1653
1654 /**
1655 * Add a "where time" statement to the query.
1656 *
1657 * @param string $column
1658 * @param string $operator
1659 * @param \DateTimeInterface|string|null $value
1660 * @param string $boolean
1661 * @return $this
1662 */
1663 public function whereTime($column, $operator, $value = null, $boolean = 'and')
1664 {
1665 [$value, $operator] = $this->prepareValueAndOperator(
1666 $value, $operator, func_num_args() === 2
1667 );
1668
1669 $value = $this->flattenValue($value);
1670
1671 if ($value instanceof DateTimeInterface) {
1672 $value = $value->format('H:i:s');
1673 }
1674
1675 return $this->addDateBasedWhere('Time', $column, $operator, $value, $boolean);
1676 }
1677
1678 /**
1679 * Add an "or where time" statement to the query.
1680 *
1681 * @param string $column
1682 * @param string $operator
1683 * @param \DateTimeInterface|string|null $value
1684 * @return $this
1685 */
1686 public function orWhereTime($column, $operator, $value = null)
1687 {
1688 [$value, $operator] = $this->prepareValueAndOperator(
1689 $value, $operator, func_num_args() === 2
1690 );
1691
1692 return $this->whereTime($column, $operator, $value, 'or');
1693 }
1694
1695 /**
1696 * Add a "where day" statement to the query.
1697 *
1698 * @param string $column
1699 * @param string $operator
1700 * @param \DateTimeInterface|string|null $value
1701 * @param string $boolean
1702 * @return $this
1703 */
1704 public function whereDay($column, $operator, $value = null, $boolean = 'and')
1705 {
1706 [$value, $operator] = $this->prepareValueAndOperator(
1707 $value, $operator, func_num_args() === 2
1708 );
1709
1710 $value = $this->flattenValue($value);
1711
1712 if ($value instanceof DateTimeInterface) {
1713 $value = $value->format('d');
1714 }
1715
1716 if (! $value instanceof Expression) {
1717 $value = sprintf('%02d', $value);
1718 }
1719
1720 return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean);
1721 }
1722
1723 /**
1724 * Add an "or where day" statement to the query.
1725 *
1726 * @param string $column
1727 * @param string $operator
1728 * @param \DateTimeInterface|string|null $value
1729 * @return $this
1730 */
1731 public function orWhereDay($column, $operator, $value = null)
1732 {
1733 [$value, $operator] = $this->prepareValueAndOperator(
1734 $value, $operator, func_num_args() === 2
1735 );
1736
1737 return $this->whereDay($column, $operator, $value, 'or');
1738 }
1739
1740 /**
1741 * Add a "where month" statement to the query.
1742 *
1743 * @param string $column
1744 * @param string $operator
1745 * @param \DateTimeInterface|string|null $value
1746 * @param string $boolean
1747 * @return $this
1748 */
1749 public function whereMonth($column, $operator, $value = null, $boolean = 'and')
1750 {
1751 [$value, $operator] = $this->prepareValueAndOperator(
1752 $value, $operator, func_num_args() === 2
1753 );
1754
1755 $value = $this->flattenValue($value);
1756
1757 if ($value instanceof DateTimeInterface) {
1758 $value = $value->format('m');
1759 }
1760
1761 if (! $value instanceof Expression) {
1762 $value = sprintf('%02d', $value);
1763 }
1764
1765 return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean);
1766 }
1767
1768 /**
1769 * Add an "or where month" statement to the query.
1770 *
1771 * @param string $column
1772 * @param string $operator
1773 * @param \DateTimeInterface|string|null $value
1774 * @return $this
1775 */
1776 public function orWhereMonth($column, $operator, $value = null)
1777 {
1778 [$value, $operator] = $this->prepareValueAndOperator(
1779 $value, $operator, func_num_args() === 2
1780 );
1781
1782 return $this->whereMonth($column, $operator, $value, 'or');
1783 }
1784
1785 /**
1786 * Add a "where year" statement to the query.
1787 *
1788 * @param string $column
1789 * @param string $operator
1790 * @param \DateTimeInterface|string|int|null $value
1791 * @param string $boolean
1792 * @return $this
1793 */
1794 public function whereYear($column, $operator, $value = null, $boolean = 'and')
1795 {
1796 [$value, $operator] = $this->prepareValueAndOperator(
1797 $value, $operator, func_num_args() === 2
1798 );
1799
1800 $value = $this->flattenValue($value);
1801
1802 if ($value instanceof DateTimeInterface) {
1803 $value = $value->format('Y');
1804 }
1805
1806 return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean);
1807 }
1808
1809 /**
1810 * Add an "or where year" statement to the query.
1811 *
1812 * @param string $column
1813 * @param string $operator
1814 * @param \DateTimeInterface|string|int|null $value
1815 * @return $this
1816 */
1817 public function orWhereYear($column, $operator, $value = null)
1818 {
1819 [$value, $operator] = $this->prepareValueAndOperator(
1820 $value, $operator, func_num_args() === 2
1821 );
1822
1823 return $this->whereYear($column, $operator, $value, 'or');
1824 }
1825
1826 /**
1827 * Add a date based (year, month, day, time) statement to the query.
1828 *
1829 * @param string $type
1830 * @param string $column
1831 * @param string $operator
1832 * @param mixed $value
1833 * @param string $boolean
1834 * @return $this
1835 */
1836 protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and')
1837 {
1838 $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value');
1839
1840 if (! $value instanceof Expression) {
1841 $this->addBinding($value, 'where');
1842 }
1843
1844 return $this;
1845 }
1846
1847 /**
1848 * Add a nested where statement to the query.
1849 *
1850 * @param \Closure $callback
1851 * @param string $boolean
1852 * @return $this
1853 */
1854 public function whereNested(Closure $callback, $boolean = 'and')
1855 {
1856 $callback($query = $this->forNestedWhere());
1857
1858 return $this->addNestedWhereQuery($query, $boolean);
1859 }
1860
1861 /**
1862 * Create a new query instance for nested where condition.
1863 *
1864 * @return \FluentBoards\Framework\Database\Query\Builder
1865 */
1866 public function forNestedWhere()
1867 {
1868 return $this->newQuery()->from($this->from);
1869 }
1870
1871 /**
1872 * Add another query builder as a nested where to the query builder.
1873 *
1874 * @param \FluentBoards\Framework\Database\Query\Builder $query
1875 * @param string $boolean
1876 * @return $this
1877 */
1878 public function addNestedWhereQuery($query, $boolean = 'and')
1879 {
1880 if (count($query->wheres)) {
1881 $type = 'Nested';
1882
1883 $this->wheres[] = compact('type', 'query', 'boolean');
1884
1885 $this->addBinding($query->getRawBindings()['where'], 'where');
1886 }
1887
1888 return $this;
1889 }
1890
1891 /**
1892 * Add a full sub-select to the query.
1893 *
1894 * @param string $column
1895 * @param string $operator
1896 * @param \Closure $callback
1897 * @param string $boolean
1898 * @return $this
1899 */
1900 protected function whereSub($column, $operator, $callback, $boolean)
1901 {
1902 $type = 'Sub';
1903
1904 if ($callback instanceof Closure) {
1905 // Once we have the query instance we can simply execute it so it can add all
1906 // of the sub-select's conditions to itself, and then we can cache it off
1907 // in the array of where clauses for the "main" parent query instance.
1908 $callback($query = $this->forSubQuery());
1909 } else {
1910 $query = $callback instanceof OrmBuilder ? $callback->toBase() : $callback;
1911 }
1912
1913 $this->wheres[] = compact(
1914 'type', 'column', 'operator', 'query', 'boolean'
1915 );
1916
1917 $this->addBinding($query->getBindings(), 'where');
1918
1919 return $this;
1920 }
1921
1922 /**
1923 * Add an exists clause to the query.
1924 *
1925 * @param \Closure $callback
1926 * @param string $boolean
1927 * @param bool $not
1928 * @return $this
1929 */
1930 public function whereExists(Closure $callback, $boolean = 'and', $not = false)
1931 {
1932 if ($callback instanceof Closure) {
1933 $query = $this->forSubQuery();
1934
1935 // Similar to the sub-select clause, we will create a new query instance so
1936 // the developer may cleanly specify the entire exists query and we will
1937 // compile the whole thing in the grammar and insert it into the SQL.
1938 $callback($query);
1939 } else {
1940 $query = $callback instanceof OrmBuilder ? $callback->toBase() : $callback;
1941 }
1942
1943 return $this->addWhereExistsQuery($query, $boolean, $not);
1944 }
1945
1946 /**
1947 * Add an or exists clause to the query.
1948 *
1949 * @param \Closure $callback
1950 * @param bool $not
1951 * @return $this
1952 */
1953 public function orWhereExists($callback, $not = false)
1954 {
1955 return $this->whereExists($callback, 'or', $not);
1956 }
1957
1958 /**
1959 * Add a where not exists clause to the query.
1960 *
1961 * @param \Closure $callback
1962 * @param string $boolean
1963 * @return $this
1964 */
1965 public function whereNotExists($callback, $boolean = 'and')
1966 {
1967 return $this->whereExists($callback, $boolean, true);
1968 }
1969
1970 /**
1971 * Add a where not exists clause to the query.
1972 *
1973 * @param \Closure $callback
1974 * @return $this
1975 */
1976 public function orWhereNotExists($callback)
1977 {
1978 return $this->orWhereExists($callback, true);
1979 }
1980
1981 /**
1982 * Add an exists clause to the query.
1983 *
1984 * @param \FluentBoards\Framework\Database\Query\Builder $query
1985 * @param string $boolean
1986 * @param bool $not
1987 * @return $this
1988 */
1989 public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false)
1990 {
1991 $type = $not ? 'NotExists' : 'Exists';
1992
1993 $this->wheres[] = compact('type', 'query', 'boolean');
1994
1995 $this->addBinding($query->getBindings(), 'where');
1996
1997 return $this;
1998 }
1999
2000 /**
2001 * Adds a where condition using row values.
2002 *
2003 * @param array $columns
2004 * @param string $operator
2005 * @param array $values
2006 * @param string $boolean
2007 * @return $this
2008 *
2009 * @throws \InvalidArgumentException
2010 */
2011 public function whereRowValues($columns, $operator, $values, $boolean = 'and')
2012 {
2013 if (count($columns) !== count($values)) {
2014 throw new InvalidArgumentException('The number of columns must match the number of values');
2015 }
2016
2017 $type = 'RowValues';
2018
2019 $this->wheres[] = compact('type', 'columns', 'operator', 'values', 'boolean');
2020
2021 $this->addBinding($this->cleanBindings($values));
2022
2023 return $this;
2024 }
2025
2026 /**
2027 * Adds an or where condition using row values.
2028 *
2029 * @param array $columns
2030 * @param string $operator
2031 * @param array $values
2032 * @return $this
2033 */
2034 public function orWhereRowValues($columns, $operator, $values)
2035 {
2036 return $this->whereRowValues($columns, $operator, $values, 'or');
2037 }
2038
2039 /**
2040 * Add a "where JSON contains" clause to the query.
2041 *
2042 * @param string $column
2043 * @param mixed $value
2044 * @param string $boolean
2045 * @param bool $not
2046 * @return $this
2047 */
2048 public function whereJsonContains($column, $value, $boolean = 'and', $not = false)
2049 {
2050 $type = 'JsonContains';
2051
2052 $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not');
2053
2054 if (! $value instanceof Expression) {
2055 $this->addBinding($this->grammar->prepareBindingForJsonContains($value));
2056 }
2057
2058 return $this;
2059 }
2060
2061 /**
2062 * Add an "or where JSON contains" clause to the query.
2063 *
2064 * @param string $column
2065 * @param mixed $value
2066 * @return $this
2067 */
2068 public function orWhereJsonContains($column, $value)
2069 {
2070 return $this->whereJsonContains($column, $value, 'or');
2071 }
2072
2073 /**
2074 * Add a "where JSON not contains" clause to the query.
2075 *
2076 * @param string $column
2077 * @param mixed $value
2078 * @param string $boolean
2079 * @return $this
2080 */
2081 public function whereJsonDoesntContain($column, $value, $boolean = 'and')
2082 {
2083 return $this->whereJsonContains($column, $value, $boolean, true);
2084 }
2085
2086 /**
2087 * Add an "or where JSON not contains" clause to the query.
2088 *
2089 * @param string $column
2090 * @param mixed $value
2091 * @return $this
2092 */
2093 public function orWhereJsonDoesntContain($column, $value)
2094 {
2095 return $this->whereJsonDoesntContain($column, $value, 'or');
2096 }
2097
2098 /**
2099 * Add a "where JSON overlaps" clause to the query.
2100 *
2101 * @param string $column
2102 * @param mixed $value
2103 * @param string $boolean
2104 * @param bool $not
2105 * @return $this
2106 */
2107 public function whereJsonOverlaps($column, $value, $boolean = 'and', $not = false)
2108 {
2109 $type = 'JsonOverlaps';
2110
2111 $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not');
2112
2113 if (! $value instanceof Expression) {
2114 $this->addBinding($this->grammar->prepareBindingForJsonContains($value));
2115 }
2116
2117 return $this;
2118 }
2119
2120 /**
2121 * Add an "or where JSON overlaps" clause to the query.
2122 *
2123 * @param string $column
2124 * @param mixed $value
2125 * @return $this
2126 */
2127 public function orWhereJsonOverlaps($column, $value)
2128 {
2129 return $this->whereJsonOverlaps($column, $value, 'or');
2130 }
2131
2132 /**
2133 * Add a "where JSON not overlap" clause to the query.
2134 *
2135 * @param string $column
2136 * @param mixed $value
2137 * @param string $boolean
2138 * @return $this
2139 */
2140 public function whereJsonDoesntOverlap($column, $value, $boolean = 'and')
2141 {
2142 return $this->whereJsonOverlaps($column, $value, $boolean, true);
2143 }
2144
2145 /**
2146 * Add an "or where JSON not overlap" clause to the query.
2147 *
2148 * @param string $column
2149 * @param mixed $value
2150 * @return $this
2151 */
2152 public function orWhereJsonDoesntOverlap($column, $value)
2153 {
2154 return $this->whereJsonDoesntOverlap($column, $value, 'or');
2155 }
2156
2157 /**
2158 * Add a clause that determines if a JSON path exists to the query.
2159 *
2160 * @param string $column
2161 * @param string $boolean
2162 * @param bool $not
2163 * @return $this
2164 */
2165 public function whereJsonContainsKey($column, $boolean = 'and', $not = false)
2166 {
2167 $type = 'JsonContainsKey';
2168
2169 $this->wheres[] = compact('type', 'column', 'boolean', 'not');
2170
2171 return $this;
2172 }
2173
2174 /**
2175 * Add an "or" clause that determines if a JSON path exists to the query.
2176 *
2177 * @param string $column
2178 * @return $this
2179 */
2180 public function orWhereJsonContainsKey($column)
2181 {
2182 return $this->whereJsonContainsKey($column, 'or');
2183 }
2184
2185 /**
2186 * Add a clause that determines if a JSON path does not exist to the query.
2187 *
2188 * @param string $column
2189 * @param string $boolean
2190 * @return $this
2191 */
2192 public function whereJsonDoesntContainKey($column, $boolean = 'and')
2193 {
2194 return $this->whereJsonContainsKey($column, $boolean, true);
2195 }
2196
2197 /**
2198 * Add an "or" clause that determines if a JSON path does not exist to the query.
2199 *
2200 * @param string $column
2201 * @return $this
2202 */
2203 public function orWhereJsonDoesntContainKey($column)
2204 {
2205 return $this->whereJsonDoesntContainKey($column, 'or');
2206 }
2207
2208 /**
2209 * Add a "where JSON length" clause to the query.
2210 *
2211 * @param string $column
2212 * @param mixed $operator
2213 * @param mixed $value
2214 * @param string $boolean
2215 * @return $this
2216 */
2217 public function whereJsonLength(
2218 $column,
2219 $operator,
2220 $value = null,
2221 $boolean = 'and'
2222 ) {
2223 $type = 'JsonLength';
2224
2225 [$value, $operator] = $this->prepareValueAndOperator(
2226 $value, $operator, func_num_args() === 2
2227 );
2228
2229 $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');
2230
2231 if (! $value instanceof Expression) {
2232 $this->addBinding((int) $this->flattenValue($value));
2233 }
2234
2235 return $this;
2236 }
2237
2238 /**
2239 * Add an "or where JSON length" clause to the query.
2240 *
2241 * @param string $column
2242 * @param mixed $operator
2243 * @param mixed $value
2244 * @return $this
2245 */
2246 public function orWhereJsonLength($column, $operator, $value = null)
2247 {
2248 [$value, $operator] = $this->prepareValueAndOperator(
2249 $value, $operator, func_num_args() === 2
2250 );
2251
2252 return $this->whereJsonLength($column, $operator, $value, 'or');
2253 }
2254
2255 /**
2256 * Handles dynamic "where" clauses to the query.
2257 *
2258 * @param string $method
2259 * @param array $parameters
2260 * @return $this
2261 */
2262 public function dynamicWhere($method, $parameters)
2263 {
2264 $finder = substr($method, 5);
2265
2266 $segments = preg_split(
2267 '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
2268 );
2269
2270 // The connector variable will determine which connector will be used for the
2271 // query condition. We will change it as we come across new boolean values
2272 // in the dynamic method strings, which could contain a number of these.
2273 $connector = 'and';
2274
2275 $index = 0;
2276
2277 foreach ($segments as $segment) {
2278 // If the segment is not a boolean connector, we can assume it is a column's name
2279 // and we will add it to the query as a new constraint as a where clause, then
2280 // we can keep iterating through the dynamic method string's segments again.
2281 if ($segment !== 'And' && $segment !== 'Or') {
2282 $this->addDynamic($segment, $connector, $parameters, $index);
2283
2284 $index++;
2285 }
2286
2287 // Otherwise, we will store the connector so we know how the next where clause we
2288 // find in the query should be connected to the previous ones, meaning we will
2289 // have the proper boolean connector to connect the next where clause found.
2290 else {
2291 $connector = $segment;
2292 }
2293 }
2294
2295 return $this;
2296 }
2297
2298 /**
2299 * Add a single dynamic where clause statement to the query.
2300 *
2301 * @param string $segment
2302 * @param string $connector
2303 * @param array $parameters
2304 * @param int $index
2305 * @return void
2306 */
2307 protected function addDynamic($segment, $connector, $parameters, $index)
2308 {
2309 // Once we have parsed out the columns and formatted the boolean operators we
2310 // are ready to add it to this query as a where clause just like any other
2311 // clause on the query. Then we'll increment the parameter index values.
2312 $bool = strtolower($connector);
2313
2314 $this->where(Str::snake($segment), '=', $parameters[$index], $bool);
2315 }
2316
2317 /**
2318 * Add a "where fulltext" clause to the query.
2319 *
2320 * @param string|string[] $columns
2321 * @param string $value
2322 * @param string $boolean
2323 * @return $this
2324 */
2325 public function whereFullText(
2326 $columns,
2327 $value,
2328 array $options = [],
2329 $boolean = 'and'
2330 ) {
2331 $type = 'Fulltext';
2332
2333 $columns = (array) $columns;
2334
2335 $this->wheres[] = compact('type', 'columns', 'value', 'options', 'boolean');
2336
2337 $this->addBinding($value);
2338
2339 return $this;
2340 }
2341
2342 /**
2343 * Add a "or where fulltext" clause to the query.
2344 *
2345 * @param string|string[] $columns
2346 * @param string $value
2347 * @return $this
2348 */
2349 public function orWhereFullText($columns, $value, array $options = [])
2350 {
2351 return $this->whereFulltext($columns, $value, $options, 'or');
2352 }
2353
2354 /**
2355 * Add a "where" clause to the query for multiple columns with "and" conditions between them.
2356 *
2357 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2358 * @param mixed $operator
2359 * @param mixed $value
2360 * @param string $boolean
2361 * @return $this
2362 */
2363 public function whereAll(
2364 $columns,
2365 $operator = null,
2366 $value = null,
2367 $boolean = 'and'
2368 ) {
2369 [$value, $operator] = $this->prepareValueAndOperator(
2370 $value, $operator, func_num_args() === 2
2371 );
2372
2373 $this->whereNested(function ($query) use ($columns, $operator, $value) {
2374 foreach ($columns as $column) {
2375 $query->where($column, $operator, $value, 'and');
2376 }
2377 }, $boolean);
2378
2379 return $this;
2380 }
2381
2382 /**
2383 * Add an "or where" clause to the query for multiple columns with "and" conditions between them.
2384 *
2385 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2386 * @param mixed $operator
2387 * @param mixed $value
2388 * @return $this
2389 */
2390 public function orWhereAll($columns, $operator = null, $value = null)
2391 {
2392 return $this->whereAll($columns, $operator, $value, 'or');
2393 }
2394
2395 /**
2396 * Add a "where" clause to the query for multiple columns with "or" conditions between them.
2397 *
2398 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2399 * @param mixed $operator
2400 * @param mixed $value
2401 * @param string $boolean
2402 * @return $this
2403 */
2404 public function whereAny(
2405 $columns,
2406 $operator = null,
2407 $value = null,
2408 $boolean = 'and'
2409 ) {
2410 [$value, $operator] = $this->prepareValueAndOperator(
2411 $value, $operator, func_num_args() === 2
2412 );
2413
2414 $this->whereNested(function ($query) use ($columns, $operator, $value) {
2415 foreach ($columns as $column) {
2416 $query->where($column, $operator, $value, 'or');
2417 }
2418 }, $boolean);
2419
2420 return $this;
2421 }
2422
2423 /**
2424 * Add an "or where" clause to the query for multiple columns with "or" conditions between them.
2425 *
2426 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2427 * @param mixed $operator
2428 * @param mixed $value
2429 * @return $this
2430 */
2431 public function orWhereAny($columns, $operator = null, $value = null)
2432 {
2433 return $this->whereAny($columns, $operator, $value, 'or');
2434 }
2435
2436 /**
2437 * Add a "where not" clause to the query for multiple columns where none of the conditions should be true.
2438 *
2439 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2440 * @param mixed $operator
2441 * @param mixed $value
2442 * @param string $boolean
2443 * @return $this
2444 */
2445 public function whereNone($columns, $operator = null, $value = null, $boolean = 'and')
2446 {
2447 return $this->whereAny($columns, $operator, $value, $boolean.' not');
2448 }
2449
2450 /**
2451 * Add an "or where not" clause to the query for multiple columns where none of the conditions should be true.
2452 *
2453 * @param \FluentBoards\Framework\Database\Query\Expression[]|string[] $columns
2454 * @param mixed $operator
2455 * @param mixed $value
2456 * @return $this
2457 */
2458 public function orWhereNone($columns, $operator = null, $value = null)
2459 {
2460 return $this->whereNone($columns, $operator, $value, 'or');
2461 }
2462
2463 /**
2464 * Add a "group by" clause to the query.
2465 *
2466 * @param array|string ...$groups
2467 * @return $this
2468 */
2469 public function groupBy(...$groups)
2470 {
2471 foreach ($groups as $group) {
2472 $this->groups = array_merge(
2473 (array) $this->groups,
2474 Arr::wrap($group)
2475 );
2476 }
2477
2478 return $this;
2479 }
2480
2481 /**
2482 * Add a raw groupBy clause to the query.
2483 *
2484 * @param string $sql
2485 * @param array $bindings
2486 * @return $this
2487 */
2488 public function groupByRaw($sql, array $bindings = [])
2489 {
2490 $this->groups[] = new Expression($sql);
2491
2492 $this->addBinding($bindings, 'groupBy');
2493
2494 return $this;
2495 }
2496
2497 /**
2498 * Add a "having" clause to the query.
2499 *
2500 * @param string $column
2501 * @param string|null $operator
2502 * @param string|null $value
2503 * @param string $boolean
2504 * @return $this
2505 */
2506 public function having($column, $operator = null, $value = null, $boolean = 'and')
2507 {
2508 $type = 'Basic';
2509
2510 if ($column instanceof ConditionExpression) {
2511 $type = 'Expression';
2512
2513 $this->havings[] = compact('type', 'column', 'boolean');
2514
2515 return $this;
2516 }
2517
2518 // Here we will make some assumptions about the operator. If only 2 values are
2519 // passed to the method, we will assume that the operator is an equals sign
2520 // and keep going. Otherwise, we'll require the operator to be passed in.
2521 [$value, $operator] = $this->prepareValueAndOperator(
2522 $value, $operator, func_num_args() === 2
2523 );
2524
2525 if ($column instanceof Closure && is_null($operator)) {
2526 return $this->havingNested($column, $boolean);
2527 }
2528
2529 // If the given operator is not found in the list of valid operators we will
2530 // assume that the developer is just short-cutting the '=' operators and
2531 // we will set the operators to '=' and set the values appropriately.
2532 if ($this->invalidOperator($operator)) {
2533 [$value, $operator] = [$operator, '='];
2534 }
2535
2536 if ($this->isBitwiseOperator($operator)) {
2537 $type = 'Bitwise';
2538 }
2539
2540 $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean');
2541
2542 if (! $value instanceof Expression) {
2543 $this->addBinding($this->flattenValue($value), 'having');
2544 }
2545
2546 return $this;
2547 }
2548
2549 /**
2550 * Add an "or having" clause to the query.
2551 *
2552 * @param string $column
2553 * @param string|null $operator
2554 * @param string|null $value
2555 * @return $this
2556 */
2557 public function orHaving($column, $operator = null, $value = null)
2558 {
2559 [$value, $operator] = $this->prepareValueAndOperator(
2560 $value, $operator, func_num_args() === 2
2561 );
2562
2563 return $this->having($column, $operator, $value, 'or');
2564 }
2565
2566 /**
2567 * Add a nested having statement to the query.
2568 *
2569 * @param \Closure $callback
2570 * @param string $boolean
2571 * @return $this
2572 */
2573 public function havingNested(Closure $callback, $boolean = 'and')
2574 {
2575 $callback($query = $this->forNestedWhere());
2576
2577 return $this->addNestedHavingQuery($query, $boolean);
2578 }
2579
2580 /**
2581 * Add another query builder as a nested having to the query builder.
2582 *
2583 * @param \FluentBoards\Framework\Database\Query\Builder $query
2584 * @param string $boolean
2585 * @return $this
2586 */
2587 public function addNestedHavingQuery($query, $boolean = 'and')
2588 {
2589 if (count($query->havings)) {
2590 $type = 'Nested';
2591
2592 $this->havings[] = compact('type', 'query', 'boolean');
2593
2594 $this->addBinding($query->getRawBindings()['having'], 'having');
2595 }
2596
2597 return $this;
2598 }
2599
2600 /**
2601 * Add a "having null" clause to the query.
2602 *
2603 * @param string|array $columns
2604 * @param string $boolean
2605 * @param bool $not
2606 * @return $this
2607 */
2608 public function havingNull($columns, $boolean = 'and', $not = false)
2609 {
2610 $type = $not ? 'NotNull' : 'Null';
2611
2612 foreach (Arr::wrap($columns) as $column) {
2613 $this->havings[] = compact('type', 'column', 'boolean');
2614 }
2615
2616 return $this;
2617 }
2618
2619 /**
2620 * Add an "or having null" clause to the query.
2621 *
2622 * @param string $column
2623 * @return $this
2624 */
2625 public function orHavingNull($column)
2626 {
2627 return $this->havingNull($column, 'or');
2628 }
2629
2630 /**
2631 * Add a "having not null" clause to the query.
2632 *
2633 * @param string|array $columns
2634 * @param string $boolean
2635 * @return $this
2636 */
2637 public function havingNotNull($columns, $boolean = 'and')
2638 {
2639 return $this->havingNull($columns, $boolean, true);
2640 }
2641
2642 /**
2643 * Add an "or having not null" clause to the query.
2644 *
2645 * @param string $column
2646 * @return $this
2647 */
2648 public function orHavingNotNull($column)
2649 {
2650 return $this->havingNotNull($column, 'or');
2651 }
2652
2653 /**
2654 * Add a "having between " clause to the query.
2655 *
2656 * @param string $column
2657 * @param array $values
2658 * @param string $boolean
2659 * @param bool $not
2660 * @return $this
2661 */
2662 public function havingBetween($column, array $values, $boolean = 'and', $not = false)
2663 {
2664 $type = 'between';
2665
2666 if ($values instanceof DatePeriod) {
2667 $values = [$values->getStartDate(), $values->getEndDate()];
2668 }
2669
2670 $this->havings[] = compact('type', 'column', 'values', 'boolean', 'not');
2671
2672 $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'having');
2673
2674 return $this;
2675 }
2676
2677 /**
2678 * Add a raw having clause to the query.
2679 *
2680 * @param string $sql
2681 * @param array $bindings
2682 * @param string $boolean
2683 * @return $this
2684 */
2685 public function havingRaw($sql, array $bindings = [], $boolean = 'and')
2686 {
2687 $type = 'Raw';
2688
2689 $this->havings[] = compact('type', 'sql', 'boolean');
2690
2691 $this->addBinding($bindings, 'having');
2692
2693 return $this;
2694 }
2695
2696 /**
2697 * Add a raw or having clause to the query.
2698 *
2699 * @param string $sql
2700 * @param array $bindings
2701 * @return $this
2702 */
2703 public function orHavingRaw($sql, array $bindings = [])
2704 {
2705 return $this->havingRaw($sql, $bindings, 'or');
2706 }
2707
2708 /**
2709 * Add an "order by" clause to the query.
2710 *
2711 * @param \Closure|\FluentBoards\Framework\Database\Orm\Builder|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Query\Expression|string $column
2712 * @param string $direction
2713 * @param array $allowedColumns
2714 * @return $this
2715 *
2716 * @throws \InvalidArgumentException
2717 */
2718 public function orderBy($column, $direction = 'asc', $allowedColumns = [])
2719 {
2720 if (!empty($allowedColumns) && !in_array($column, $allowedColumns, true)) {
2721 throw new LogicException(
2722 "Ordering by `$column` is not allowed for this query."
2723 );
2724 }
2725
2726 if (!preg_match('/^[a-zA-Z0-9_\.]+$/', $column)) {
2727 throw new LogicException("Invalid column name `$column`.");
2728 }
2729
2730 if ($this->isQueryable($column)) {
2731 [$query, $bindings] = $this->createSub($column);
2732
2733 $column = new Expression('('.$query.')');
2734
2735 $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order');
2736 }
2737
2738 $direction = strtolower($direction);
2739
2740 if (! in_array($direction, ['asc', 'desc'], true)) {
2741 throw new InvalidArgumentException(
2742 'Order direction must be "asc" or "desc".'
2743 );
2744 }
2745
2746 $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
2747 'column' => $column,
2748 'direction' => $direction,
2749 ];
2750
2751 return $this;
2752 }
2753
2754 /**
2755 * Add a descending "order by" clause to the query.
2756 *
2757 * @param \Closure|\FluentBoards\Framework\Database\Orm\Builder|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Query\Expression|string $column
2758 * @return $this
2759 */
2760 public function orderByDesc($column)
2761 {
2762 return $this->orderBy($column, 'desc');
2763 }
2764
2765 /**
2766 * Add an "order by" clause for a timestamp to the query.
2767 *
2768 * @param \Closure|\FluentBoards\Framework\Database\Orm\Builder|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Query\Expression|string $column
2769 * @return $this
2770 */
2771 public function latest($column = 'created_at')
2772 {
2773 return $this->orderBy($column, 'desc');
2774 }
2775
2776 /**
2777 * Add an "order by" clause for a timestamp to the query.
2778 *
2779 * @param \Closure|\FluentBoards\Framework\Database\Orm\Builder|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Query\Expression|string $column
2780 * @return $this
2781 */
2782 public function oldest($column = 'created_at')
2783 {
2784 return $this->orderBy($column, 'asc');
2785 }
2786
2787 /**
2788 * Put the query's results in random order.
2789 *
2790 * @param string $seed
2791 * @return $this
2792 */
2793 public function inRandomOrder($seed = '')
2794 {
2795 return $this->orderByRaw($this->grammar->compileRandom($seed));
2796 }
2797
2798 /**
2799 * Add a raw "order by" clause to the query.
2800 *
2801 * @param string $sql
2802 * @param array $bindings
2803 * @return $this
2804 */
2805 public function orderByRaw($sql, $bindings = [])
2806 {
2807 $type = 'Raw';
2808
2809 $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact(
2810 'type', 'sql'
2811 );
2812
2813 $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order');
2814
2815 return $this;
2816 }
2817
2818 /**
2819 * Alias to set the "offset" value of the query.
2820 *
2821 * @param int $value
2822 * @return $this
2823 */
2824 public function skip($value)
2825 {
2826 return $this->offset($value);
2827 }
2828
2829 /**
2830 * Set the "offset" value of the query.
2831 *
2832 * @param int $value
2833 * @return $this
2834 */
2835 public function offset($value)
2836 {
2837 $property = $this->unions ? 'unionOffset' : 'offset';
2838
2839 $this->$property = max(0, (int) $value);
2840
2841 return $this;
2842 }
2843
2844 /**
2845 * Alias to set the "limit" value of the query.
2846 *
2847 * @param int $value
2848 * @return $this
2849 */
2850 public function take($value)
2851 {
2852 return $this->limit($value);
2853 }
2854
2855 /**
2856 * Set the "limit" value of the query.
2857 *
2858 * @param int $value
2859 * @return $this
2860 */
2861 public function limit($value)
2862 {
2863 $property = $this->unions ? 'unionLimit' : 'limit';
2864
2865 if ($value >= 0) {
2866 $this->$property = ! is_null($value) ? (int) $value : null;
2867 }
2868
2869 return $this;
2870 }
2871
2872 /**
2873 * Add a "group limit" clause to the query.
2874 *
2875 * @param int $value
2876 * @param string $column
2877 * @return $this
2878 */
2879 public function groupLimit($value, $column)
2880 {
2881 if ($value >= 0) {
2882 $this->groupLimit = compact('value', 'column');
2883 }
2884
2885 return $this;
2886 }
2887
2888 /**
2889 * Set the limit and offset for a given page.
2890 *
2891 * @param int $page
2892 * @param int $perPage
2893 * @return $this
2894 */
2895 public function forPage($page, $perPage = 15)
2896 {
2897 return $this->offset(($page - 1) * $perPage)->limit($perPage);
2898 }
2899
2900 /**
2901 * Constrain the query to the previous "page" of results before a given ID.
2902 *
2903 * @param int $perPage
2904 * @param int|null $lastId
2905 * @param string $column
2906 * @return $this
2907 */
2908 public function forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id')
2909 {
2910 $this->orders = $this->removeExistingOrdersFor($column);
2911
2912 if (! is_null($lastId)) {
2913 $this->where($column, '<', $lastId);
2914 }
2915
2916 return $this->orderBy($column, 'desc')
2917 ->limit($perPage);
2918 }
2919
2920 /**
2921 * Constrain the query to the next "page" of results after a given ID.
2922 *
2923 * @param int $perPage
2924 * @param int|null $lastId
2925 * @param string $column
2926 * @return $this
2927 */
2928 public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
2929 {
2930 $this->orders = $this->removeExistingOrdersFor($column);
2931
2932 if (! is_null($lastId)) {
2933 $this->where($column, '>', $lastId);
2934 }
2935
2936 return $this->orderBy($column, 'asc')
2937 ->limit($perPage);
2938 }
2939
2940 /**
2941 * Remove all existing orders and optionally add a new order.
2942 *
2943 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Query\Expression|string|null $column
2944 * @param string $direction
2945 * @return $this
2946 */
2947 public function reorder($column = null, $direction = 'asc')
2948 {
2949 $this->orders = null;
2950 $this->unionOrders = null;
2951 $this->bindings['order'] = [];
2952 $this->bindings['unionOrder'] = [];
2953
2954 if ($column) {
2955 return $this->orderBy($column, $direction);
2956 }
2957
2958 return $this;
2959 }
2960
2961 /**
2962 * Get an array with all orders with a given column removed.
2963 *
2964 * @param string $column
2965 * @return array
2966 */
2967 protected function removeExistingOrdersFor($column)
2968 {
2969 return Collection::make($this->orders)
2970 ->reject(function ($order) use ($column) {
2971 return isset($order['column'])
2972 ? $order['column'] === $column : false;
2973 })->values()->all();
2974 }
2975
2976 /**
2977 * Add a union statement to the query.
2978 *
2979 * @param \FluentBoards\Framework\Database\Query\Builder|\Closure $query
2980 * @param bool $all
2981 * @return $this
2982 */
2983 public function union($query, $all = false)
2984 {
2985 if ($query instanceof Closure) {
2986 $query($query = $this->newQuery());
2987 }
2988
2989 $this->unions[] = compact('query', 'all');
2990
2991 $this->addBinding($query->getBindings(), 'union');
2992
2993 return $this;
2994 }
2995
2996 /**
2997 * Add a union all statement to the query.
2998 *
2999 * @param \FluentBoards\Framework\Database\Query\Builder|\Closure $query
3000 * @return $this
3001 */
3002 public function unionAll($query)
3003 {
3004 return $this->union($query, true);
3005 }
3006
3007 /**
3008 * Lock the selected rows in the table.
3009 *
3010 * @param string|bool $value
3011 * @return $this
3012 */
3013 public function lock($value = true)
3014 {
3015 $this->lock = $value;
3016
3017 if (! is_null($this->lock)) {
3018 $this->useWritePdo();
3019 }
3020
3021 return $this;
3022 }
3023
3024 /**
3025 * Lock the selected rows in the table for updating.
3026 *
3027 * @return \FluentBoards\Framework\Database\Query\Builder
3028 */
3029 public function lockForUpdate()
3030 {
3031 return $this->lock(true);
3032 }
3033
3034 /**
3035 * Share lock the selected rows in the table.
3036 *
3037 * @return \FluentBoards\Framework\Database\Query\Builder
3038 */
3039 public function sharedLock()
3040 {
3041 return $this->lock(false);
3042 }
3043
3044 /**
3045 * Register a closure to be invoked before the query is executed.
3046 *
3047 * @param callable $callback
3048 * @return $this
3049 */
3050 public function beforeQuery(callable $callback)
3051 {
3052 $this->beforeQueryCallbacks[] = $callback;
3053
3054 return $this;
3055 }
3056
3057 /**
3058 * Invoke the "before query" modification callbacks.
3059 *
3060 * @return void
3061 */
3062 public function applyBeforeQueryCallbacks()
3063 {
3064 foreach ($this->beforeQueryCallbacks as $callback) {
3065 $callback($this);
3066 }
3067
3068 $this->beforeQueryCallbacks = [];
3069 }
3070
3071 /**
3072 * Register a closure to be invoked after the query is executed.
3073 *
3074 * @param \Closure $callback
3075 * @return $this
3076 */
3077 public function afterQuery(Closure $callback)
3078 {
3079 $this->afterQueryCallbacks[] = $callback;
3080
3081 return $this;
3082 }
3083
3084 /**
3085 * Invoke the "after query" modification callbacks.
3086 *
3087 * @param mixed $result
3088 * @return mixed
3089 */
3090 public function applyAfterQueryCallbacks($result)
3091 {
3092 foreach ($this->afterQueryCallbacks as $afterQueryCallback) {
3093 $result = $afterQueryCallback($result) ?: $result;
3094 }
3095
3096 return $result;
3097 }
3098
3099 /**
3100 * Get the SQL representation of the query.
3101 *
3102 * @return string
3103 */
3104 public function toSql()
3105 {
3106 $this->applyBeforeQueryCallbacks();
3107
3108 return $this->grammar->compileSelect($this);
3109 }
3110
3111 /**
3112 * Get the raw SQL representation of the query with embedded bindings.
3113 *
3114 * @return string
3115 */
3116 public function toRawSql()
3117 {
3118 return $this->grammar->substituteBindingsIntoRawSql(
3119 $this->toSql(), $this->connection->prepareBindings($this->getBindings())
3120 );
3121 }
3122
3123 /**
3124 * Execute a query for a single record by ID.
3125 *
3126 * @param int|string $id
3127 * @param array $columns
3128 * @return mixed|static
3129 */
3130 public function find($id, $columns = ['*'])
3131 {
3132 return $this->where('id', '=', $id)->first($columns);
3133 }
3134
3135 /**
3136 * Execute a query for a single record by ID or call a callback.
3137 *
3138 * @template TValue
3139 *
3140 * @param mixed $id
3141 * @param (\Closure(): TValue)|list<string>|string $columns
3142 * @param (\Closure(): TValue)|null $callback
3143 * @return object|TValue
3144 */
3145 public function findOr($id, $columns = ['*'], ?Closure $callback = null)
3146 {
3147 if ($columns instanceof Closure) {
3148 $callback = $columns;
3149
3150 $columns = ['*'];
3151 }
3152
3153 if (! is_null($data = $this->find($id, $columns))) {
3154 return $data;
3155 }
3156
3157 return $callback();
3158 }
3159
3160 /**
3161 * Get a single column's value from the first result of a query.
3162 *
3163 * @param string $column
3164 * @return mixed
3165 */
3166 public function value($column)
3167 {
3168 $result = (array) $this->first([$column]);
3169
3170 return count($result) > 0 ? reset($result) : null;
3171 }
3172
3173 /**
3174 * Get a single expression value from the first result of a query.
3175 *
3176 * @param string $expression
3177 * @param array $bindings
3178 * @return mixed
3179 */
3180 public function rawValue(string $expression, array $bindings = [])
3181 {
3182 $result = (array) $this->selectRaw($expression, $bindings)->first();
3183
3184 return count($result) > 0 ? reset($result) : null;
3185 }
3186
3187 /**
3188 * Get a single column's value from the first result of a query if it's the sole matching record.
3189 *
3190 * @param string $column
3191 * @return mixed
3192 *
3193 * @throws \FluentBoards\Framework\Database\RecordsNotFoundException
3194 * @throws \FluentBoards\Framework\Database\MultipleRecordsFoundException
3195 */
3196 public function soleValue($column)
3197 {
3198 $result = (array) $this->sole([$column]);
3199
3200 return reset($result);
3201 }
3202
3203 /**
3204 * Execute the query as a "select" statement.
3205 *
3206 * @param array|string $columns
3207 * @return \FluentBoards\Framework\Support\Collection
3208 */
3209 public function get($columns = ['*'])
3210 {
3211 $items = Helper::collect($this->onceWithColumns(Arr::wrap($columns), function () {
3212 return $this->processor->processSelect($this, $this->runSelect());
3213 }));
3214
3215 return $this->applyAfterQueryCallbacks(
3216 isset($this->groupLimit) ? $this->withoutGroupLimitKeys($items) : $items
3217 );
3218 }
3219
3220 /**
3221 * Run the query as a "select" statement against the connection.
3222 *
3223 * @return array
3224 */
3225 protected function runSelect()
3226 {
3227 return $this->connection->select(
3228 $this->toSql(), $this->getBindings(), ! $this->useWritePdo
3229 );
3230 }
3231
3232 /**
3233 * Remove the group limit keys from the results in the collection.
3234 *
3235 * @param \FluentBoards\Framework\Support\Collection $items
3236 * @return \FluentBoards\Framework\Support\Collection
3237 */
3238 protected function withoutGroupLimitKeys($items)
3239 {
3240 $keysToRemove = ['laravel_row'];
3241
3242 if (is_string($this->groupLimit['column'])) {
3243 $column = Helper::last(explode('.', $this->groupLimit['column']));
3244
3245 $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap($column);
3246 $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap('pivot_'.$column);
3247 }
3248
3249 $items->each(function ($item) use ($keysToRemove) {
3250 foreach ($keysToRemove as $key) {
3251 unset($item->$key);
3252 }
3253 });
3254
3255 return $items;
3256 }
3257
3258 /**
3259 * Paginate the given query into a simple paginator.
3260 *
3261 * @param int $perPage
3262 * @param array $columns
3263 * @param string $pageName
3264 * @param int|null $page
3265 * @param int|null $total
3266 * @return \FluentBoards\Framework\Pagination\LengthAwarePaginatorInterface
3267 */
3268 public function paginate(
3269 $perPage = 15,
3270 $columns = ['*'],
3271 $pageName = 'page',
3272 $page = null,
3273 $total = null
3274 ) {
3275 $page = $page ?: Paginator::resolveCurrentPage($pageName);
3276
3277 $total = Helper::value($total) ?? $this->getCountForPagination();
3278
3279 $perPage = $perPage instanceof Closure ? $perPage($total) : $perPage;
3280
3281 $results = $total
3282 ? $this->forPage($page, $perPage)->get($columns)
3283 : Helper::collect();
3284
3285 return $this->paginator($results, $total, $perPage, $page, [
3286 'path' => Paginator::resolveCurrentPath(),
3287 'pageName' => $pageName,
3288 ]);
3289 }
3290
3291 /**
3292 * Get a paginator only supporting simple next and previous links.
3293 *
3294 * This is more efficient on larger data-sets, etc.
3295 *
3296 * @param int $perPage
3297 * @param array $columns
3298 * @param string $pageName
3299 * @param int|null $page
3300 * @return \FluentBoards\Framework\Pagination\PaginatorInterface
3301 */
3302 public function simplePaginate(
3303 $perPage = 15,
3304 $columns = ['*'],
3305 $pageName = 'page',
3306 $page = null
3307 )
3308 {
3309 $page = $page ?: Paginator::resolveCurrentPage($pageName);
3310
3311 $this->offset(($page - 1) * $perPage)->limit($perPage + 1);
3312
3313 return $this->simplePaginator($this->get($columns), $perPage, $page, [
3314 'path' => Paginator::resolveCurrentPath(),
3315 'pageName' => $pageName,
3316 ]);
3317 }
3318
3319 /**
3320 * Get a cursor paginator for efficient pagination of large datasets.
3321 *
3322 * Cursor pagination uses a unique column value (or multiple columns)
3323 * as a pointer, allowing for consistent, efficient
3324 * navigation without large OFFSETs.
3325 *
3326 * @param int|null $perPage
3327 * @param array $columns
3328 * @param string $cursorName
3329 * @param \FluentBoards\Framework\Pagination\Cursor|string|null $cursor
3330 * @return \FluentBoards\Framework\Pagination\CursorPaginatorInterface
3331 */
3332 public function cursorPaginate(
3333 $perPage = 15,
3334 $columns = ['*'],
3335 $cursorName = 'cursor',
3336 $cursor = null
3337 )
3338 {
3339 return $this->paginateUsingCursor(
3340 $perPage, $columns, $cursorName, $cursor
3341 );
3342 }
3343
3344 /**
3345 * Ensure the proper order by required for cursor pagination.
3346 *
3347 * @param bool $shouldReverse
3348 * @return \FluentBoards\Framework\Support\Collection
3349 */
3350 protected function ensureOrderForCursorPagination($shouldReverse = false)
3351 {
3352 if (empty($this->orders) && empty($this->unionOrders)) {
3353 $this->enforceOrderBy();
3354 }
3355
3356 $reverseDirection = function ($order) {
3357 if (! isset($order['direction'])) {
3358 return $order;
3359 }
3360
3361 $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';
3362
3363 return $order;
3364 };
3365
3366 if ($shouldReverse) {
3367 $this->orders = Helper::collect($this->orders)->map($reverseDirection)->toArray();
3368 $this->unionOrders = Helper::collect($this->unionOrders)->map($reverseDirection)->toArray();
3369 }
3370
3371 $orders = ! empty($this->unionOrders) ? $this->unionOrders : $this->orders;
3372
3373 return Helper::collect($orders)
3374 ->filter(fn ($order) => Arr::has($order, 'direction'))
3375 ->values();
3376 }
3377
3378 /**
3379 * Get the count of the total records for the paginator.
3380 *
3381 * @param array $columns
3382 * @return int
3383 */
3384 public function getCountForPagination($columns = ['*'])
3385 {
3386 $results = $this->runPaginationCountQuery($columns);
3387
3388 // Once we have run the pagination count query, we will get the resulting count and
3389 // take into account what type of query it was. When there is a group by we will
3390 // just return the count of the entire results set since that will be correct.
3391 if (! isset($results[0])) {
3392 return 0;
3393 } elseif (is_object($results[0])) {
3394 return (int) $results[0]->aggregate;
3395 }
3396
3397 return (int) array_change_key_case((array) $results[0])['aggregate'];
3398 }
3399
3400 /**
3401 * Run a pagination count query.
3402 *
3403 * @param array $columns
3404 * @return array
3405 */
3406 protected function runPaginationCountQuery($columns = ['*'])
3407 {
3408 if ($this->groups || $this->havings) {
3409 $clone = $this->cloneForPaginationCount();
3410
3411 if (is_null($clone->columns) && ! empty($this->joins)) {
3412 $clone->select($this->from.'.*');
3413 }
3414
3415 return $this->newQuery()
3416 ->from(new Expression('('.$clone->toSql().') as '.$this->grammar->wrap('aggregate_table')))
3417 ->mergeBindings($clone)
3418 ->setAggregate('count', $this->withoutSelectAliases($columns))
3419 ->get()->all();
3420 }
3421
3422 $without = $this->unions ? ['unionOrders', 'unionLimit', 'unionOffset'] : ['columns', 'orders', 'limit', 'offset'];
3423
3424 return $this->cloneWithout($without)
3425 ->cloneWithoutBindings($this->unions ? ['unionOrder'] : ['select', 'order'])
3426 ->setAggregate('count', $this->withoutSelectAliases($columns))
3427 ->get()->all();
3428 }
3429
3430 /**
3431 * Clone the existing query instance for usage in a pagination subquery.
3432 *
3433 * @return self
3434 */
3435 protected function cloneForPaginationCount()
3436 {
3437 return $this->cloneWithout(['orders', 'limit', 'offset'])
3438 ->cloneWithoutBindings(['order']);
3439 }
3440
3441 /**
3442 * Remove the column aliases since they will break count queries.
3443 *
3444 * @param array $columns
3445 * @return array
3446 */
3447 protected function withoutSelectAliases(array $columns)
3448 {
3449 return array_map(function ($column) {
3450 return is_string($column) && ($aliasPosition = stripos($column, ' as ')) !== false
3451 ? substr($column, 0, $aliasPosition) : $column;
3452 }, $columns);
3453 }
3454
3455 /**
3456 * Get a lazy collection for the given query.
3457 *
3458 * @return \FluentBoards\Framework\Support\LazyCollection
3459 */
3460 public function cursor()
3461 {
3462 if (is_null($this->columns)) {
3463 $this->columns = ['*'];
3464 }
3465
3466 return (new LazyCollection(function () {
3467 yield from $this->connection->cursor(
3468 $this->toSql(), $this->getBindings(), ! $this->useWritePdo
3469 );
3470 }))->map(function ($item) {
3471 return $this->applyAfterQueryCallbacks(
3472 Helper::collect([$item])
3473 )->first();
3474 })->reject(fn ($item) => is_null($item));
3475 }
3476
3477 /**
3478 * Get a lazy collection for the given query.
3479 *
3480 * @return \FluentBoards\Framework\Support\LazyCollection
3481 */
3482 public function rawCursor()
3483 {
3484 if (is_null($this->columns)) {
3485 $this->columns = ['*'];
3486 }
3487
3488 return (new LazyCollection(function () {
3489 yield from $this->connection->rawCursor(
3490 $this->toSql(), $this->getBindings(), ! $this->useWritePdo
3491 );
3492 }))->map(function ($item) {
3493 return $this->applyAfterQueryCallbacks(
3494 Helper::collect([$item])
3495 )->first();
3496 })->reject(fn ($item) => is_null($item));
3497 }
3498
3499 /**
3500 * Throw an exception if the query doesn't have an orderBy clause.
3501 *
3502 * @return void
3503 *
3504 * @throws \RuntimeException
3505 */
3506 protected function enforceOrderBy()
3507 {
3508 if (empty($this->orders) && empty($this->unionOrders)) {
3509 throw new RuntimeException(
3510 'You must specify an orderBy clause when using this function.'
3511 );
3512 }
3513 }
3514
3515 /**
3516 * Get a collection instance containing the values of a given column.
3517 *
3518 * @param string $column
3519 * @param string|null $key
3520 * @return \FluentBoards\Framework\Support\Collection
3521 */
3522 public function pluck($column, $key = null)
3523 {
3524 // First, we will need to select the results of the query accounting for the
3525 // given columns / key. Once we have the results, we will be able to take
3526 // the results and get the exact data that was requested for the query.
3527 $queryResult = $this->onceWithColumns(
3528 is_null($key) ? [$column] : [$column, $key],
3529 function () {
3530 return $this->processor->processSelect(
3531 $this, $this->runSelect()
3532 );
3533 }
3534 );
3535
3536 if (empty($queryResult)) {
3537 return Helper::collect();
3538 }
3539
3540 // If the columns are qualified with a table or have an alias, we cannot use
3541 // those directly in the "pluck" operations since the results from the DB
3542 // are only keyed by the column itself. We'll strip the table out here.
3543 $column = $this->stripTableForPluck($column);
3544
3545 $key = $this->stripTableForPluck($key);
3546
3547 return $this->applyAfterQueryCallbacks(
3548 is_array($queryResult[0])
3549 ? $this->pluckFromArrayColumn($queryResult, $column, $key)
3550 : $this->pluckFromObjectColumn($queryResult, $column, $key)
3551 );
3552 }
3553
3554 /**
3555 * Strip off the table name or alias from a column identifier.
3556 *
3557 * @param string $column
3558 * @return string|null
3559 */
3560 protected function stripTableForPluck($column)
3561 {
3562 if (is_null($column)) {
3563 return $column;
3564 }
3565
3566 $columnString = $column instanceof Expression
3567 ? $this->grammar->getValue($column)
3568 : $column;
3569
3570 $separator = str_contains(strtolower($columnString), ' as ') ? ' as ' : '\.';
3571
3572 return Helper::last(preg_split('~'.$separator.'~i', $columnString));
3573 }
3574
3575 /**
3576 * Retrieve column values from rows represented as objects.
3577 *
3578 * @param array $queryResult
3579 * @param string $column
3580 * @param string $key
3581 * @return \FluentBoards\Framework\Support\Collection
3582 */
3583 protected function pluckFromObjectColumn($queryResult, $column, $key)
3584 {
3585 $results = [];
3586
3587 if (is_null($key)) {
3588 foreach ($queryResult as $row) {
3589 $results[] = $row->$column;
3590 }
3591 } else {
3592 foreach ($queryResult as $row) {
3593 $results[$row->$key] = $row->$column;
3594 }
3595 }
3596
3597 return Helper::collect($results);
3598 }
3599
3600 /**
3601 * Retrieve column values from rows represented as arrays.
3602 *
3603 * @param array $queryResult
3604 * @param string $column
3605 * @param string $key
3606 * @return \FluentBoards\Framework\Support\Collection
3607 */
3608 protected function pluckFromArrayColumn($queryResult, $column, $key)
3609 {
3610 $results = [];
3611
3612 if (is_null($key)) {
3613 foreach ($queryResult as $row) {
3614 $results[] = $row[$column];
3615 }
3616 } else {
3617 foreach ($queryResult as $row) {
3618 $results[$row[$key]] = $row[$column];
3619 }
3620 }
3621
3622 return Helper::collect($results);
3623 }
3624
3625 /**
3626 * Concatenate values of a given column as a string.
3627 *
3628 * @param string $column
3629 * @param string $glue
3630 * @return string
3631 */
3632 public function implode($column, $glue = '')
3633 {
3634 return $this->pluck($column)->implode($glue);
3635 }
3636
3637 /**
3638 * Determine if any rows exist for the current query.
3639 *
3640 * @return bool
3641 */
3642 public function exists()
3643 {
3644 $this->applyBeforeQueryCallbacks();
3645
3646 $results = $this->connection->select(
3647 $this->grammar->compileExists($this), $this->getBindings(), ! $this->useWritePdo
3648 );
3649
3650 // If the results has rows, we will get the row and see if the exists column is a
3651 // boolean true. If there is no results for this query we will return false as
3652 // there are no rows for this query at all and we can return that info here.
3653 if (isset($results[0])) {
3654 $results = (array) $results[0];
3655
3656 return (bool) $results['exists'];
3657 }
3658
3659 return false;
3660 }
3661
3662 /**
3663 * Determine if no rows exist for the current query.
3664 *
3665 * @return bool
3666 */
3667 public function doesntExist()
3668 {
3669 return ! $this->exists();
3670 }
3671
3672 /**
3673 * Execute the given callback if no rows exist for the current query.
3674 *
3675 * @param \Closure $callback
3676 * @return mixed
3677 */
3678 public function existsOr(Closure $callback)
3679 {
3680 return $this->exists() ? true : $callback();
3681 }
3682
3683 /**
3684 * Execute the given callback if rows exist for the current query.
3685 *
3686 * @param \Closure $callback
3687 * @return mixed
3688 */
3689 public function doesntExistOr(Closure $callback)
3690 {
3691 return $this->doesntExist() ? true : $callback();
3692 }
3693
3694 /**
3695 * Retrieve the "count" result of the query.
3696 *
3697 * @param string $columns
3698 * @return int
3699 */
3700 public function count($columns = '*')
3701 {
3702 return (int) $this->aggregate(__FUNCTION__, Arr::wrap($columns));
3703 }
3704
3705 /**
3706 * Retrieve the minimum value of a given column.
3707 *
3708 * @param string $column
3709 * @return mixed
3710 */
3711 public function min($column)
3712 {
3713 return $this->aggregate(__FUNCTION__, [$column]);
3714 }
3715
3716 /**
3717 * Retrieve the maximum value of a given column.
3718 *
3719 * @param string $column
3720 * @return mixed
3721 */
3722 public function max($column)
3723 {
3724 return $this->aggregate(__FUNCTION__, [$column]);
3725 }
3726
3727 /**
3728 * Retrieve the sum of the values of a given column.
3729 *
3730 * @param string $column
3731 * @return mixed
3732 */
3733 public function sum($column)
3734 {
3735 $result = $this->aggregate(__FUNCTION__, [$column]);
3736
3737 return $result ?: 0;
3738 }
3739
3740 /**
3741 * Retrieve the average of the values of a given column.
3742 *
3743 * @param string $column
3744 * @return mixed
3745 */
3746 public function avg($column)
3747 {
3748 return $this->aggregate(__FUNCTION__, [$column]);
3749 }
3750
3751 /**
3752 * Alias for the "avg" method.
3753 *
3754 * @param string $column
3755 * @return mixed
3756 */
3757 public function average($column)
3758 {
3759 return $this->avg($column);
3760 }
3761
3762 /**
3763 * Execute an aggregate function on the database.
3764 *
3765 * @param string $function
3766 * @param array $columns
3767 * @return mixed
3768 */
3769 public function aggregate($function, $columns = ['*'])
3770 {
3771 $results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns'])
3772 ->cloneWithoutBindings($this->unions || $this->havings ? [] : ['select'])
3773 ->setAggregate($function, $columns)
3774 ->get($columns);
3775
3776 if (! $results->isEmpty()) {
3777 return array_change_key_case((array) $results[0])['aggregate'];
3778 }
3779 }
3780
3781 /**
3782 * Execute a numeric aggregate function on the database.
3783 *
3784 * @param string $function
3785 * @param array $columns
3786 * @return float|int
3787 */
3788 public function numericAggregate($function, $columns = ['*'])
3789 {
3790 $result = $this->aggregate($function, $columns);
3791
3792 // If there is no result, we can obviously just return 0 here. Next, we will check
3793 // if the result is an integer or float. If it is already one of these two data
3794 // types we can just return the result as-is, otherwise we will convert this.
3795 if (! $result) {
3796 return 0;
3797 }
3798
3799 if (is_int($result) || is_float($result)) {
3800 return $result;
3801 }
3802
3803 // If the result doesn't contain a decimal place, we will assume it is an int then
3804 // cast it to one. When it does we will cast it to a float since it needs to be
3805 // cast to the expected data type for the developers out of pure convenience.
3806 return ! str_contains((string) $result, '.')
3807 ? (int) $result : (float) $result;
3808 }
3809
3810 /**
3811 * Set the aggregate property without running the query.
3812 *
3813 * @param string $function
3814 * @param array $columns
3815 * @return $this
3816 */
3817 protected function setAggregate($function, $columns)
3818 {
3819 $this->aggregate = compact('function', 'columns');
3820
3821 if (empty($this->groups)) {
3822 $this->orders = null;
3823
3824 $this->bindings['order'] = [];
3825 }
3826
3827 return $this;
3828 }
3829
3830 /**
3831 * Execute the given callback while selecting the given columns.
3832 *
3833 * After running the callback, the columns are reset to the original value.
3834 *
3835 * @param array $columns
3836 * @param callable $callback
3837 * @return mixed
3838 */
3839 protected function onceWithColumns($columns, $callback)
3840 {
3841 $original = $this->columns;
3842
3843 if (is_null($original)) {
3844 $this->columns = $columns;
3845 }
3846
3847 $result = $callback();
3848
3849 $this->columns = $original;
3850
3851 return $result;
3852 }
3853
3854 /**
3855 * Insert new records into the database.
3856 *
3857 * @param array $values
3858 * @return bool
3859 */
3860 public function insert(array $values)
3861 {
3862 // Since every insert gets treated like a batch insert, we will make sure the
3863 // bindings are structured in a way that is convenient when building these
3864 // inserts statements by verifying these elements are actually an array.
3865 if (empty($values)) {
3866 return true;
3867 }
3868
3869 if (! is_array(reset($values))) {
3870 $values = [$values];
3871 }
3872
3873 // Here, we will sort the insert keys for every record so that each insert is
3874 // in the same order for the record. We need to make sure this is the case
3875 // so there are not any errors or problems when inserting these records.
3876 else {
3877 foreach ($values as $key => $value) {
3878 ksort($value);
3879
3880 $values[$key] = $value;
3881 }
3882 }
3883
3884 $this->applyBeforeQueryCallbacks();
3885
3886 // Finally, we will run this query against the database connection and return
3887 // the results. We will need to also flatten these bindings before running
3888 // the query so they are all in one huge, flattened array for execution.
3889 return $this->connection->insert(
3890 $this->grammar->compileInsert($this, $values),
3891 $this->cleanBindings(Arr::flatten($values, 1))
3892 );
3893 }
3894
3895 /**
3896 * Insert new records into the database while ignoring errors.
3897 *
3898 * @param array $values
3899 * @return int
3900 */
3901 public function insertOrIgnore(array $values)
3902 {
3903 if (empty($values)) {
3904 return 0;
3905 }
3906
3907 if (! is_array(reset($values))) {
3908 $values = [$values];
3909 } else {
3910 foreach ($values as $key => $value) {
3911 ksort($value);
3912
3913 $values[$key] = $value;
3914 }
3915 }
3916
3917 $this->applyBeforeQueryCallbacks();
3918
3919 return $this->connection->affectingStatement(
3920 $this->grammar->compileInsertOrIgnore($this, $values),
3921 $this->cleanBindings(Arr::flatten($values, 1))
3922 );
3923 }
3924
3925 /**
3926 * Insert a new record and get the value of the primary key.
3927 *
3928 * @param array $values
3929 * @param string|null $sequence
3930 * @return int
3931 */
3932 public function insertGetId(array $values, $sequence = null)
3933 {
3934 $this->applyBeforeQueryCallbacks();
3935
3936 $sql = $this->grammar->compileInsertGetId($this, $values, $sequence);
3937
3938 $values = $this->cleanBindings($values);
3939
3940 return $this->processor->processInsertGetId($this, $sql, $values, $sequence);
3941 }
3942
3943 /**
3944 * Insert new records into the table using a subquery.
3945 *
3946 * @param array $columns
3947 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|string $query
3948 * @return int
3949 */
3950 public function insertUsing(array $columns, $query)
3951 {
3952 $this->applyBeforeQueryCallbacks();
3953
3954 [$sql, $bindings] = $this->createSub($query);
3955
3956 return $this->connection->affectingStatement(
3957 $this->grammar->compileInsertUsing($this, $columns, $sql),
3958 $this->cleanBindings($bindings)
3959 );
3960 }
3961
3962 /**
3963 * Insert new records into the table using a subquery while ignoring errors.
3964 *
3965 * @param array<string> $columns
3966 * @param \Closure|\FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder|string $query
3967 * @return int|bool Returns the number of affected rows or false on failure
3968 */
3969 public function insertOrIgnoreUsing(array $columns, $query)
3970 {
3971 $this->applyBeforeQueryCallbacks();
3972
3973 [$sql, $bindings] = $this->createSub($query);
3974
3975 return $this->connection->affectingStatement(
3976 $this->grammar->compileInsertOrIgnoreUsing($this, $columns, $sql),
3977 $this->cleanBindings($bindings)
3978 );
3979 }
3980
3981 /**
3982 * Update records in the database.
3983 *
3984 * @param array $values
3985 * @return int
3986 */
3987 public function update(array $values)
3988 {
3989 $this->applyBeforeQueryCallbacks();
3990
3991 $values = Helper::collect($values)->map(function ($value) {
3992 if (! $value instanceof Builder) {
3993 return ['value' => $value, 'bindings' => $value];
3994 }
3995
3996 [$query, $bindings] = $this->parseSub($value);
3997
3998 return ['value' => new Expression("({$query})"), 'bindings' => fn () => $bindings];
3999 });
4000
4001 $sql = $this->grammar->compileUpdate($this, $values->map(fn ($value) => $value['value'])->all());
4002
4003 return $this->connection->update($sql, $this->cleanBindings(
4004 $this->grammar->prepareBindingsForUpdate($this->bindings, $values->map(fn ($value) => $value['bindings'])->all())
4005 ));
4006 }
4007
4008 /**
4009 * Update records in a PostgreSQL database using the update from syntax.
4010 *
4011 * @param array $values
4012 * @return int
4013 */
4014 public function updateFrom(array $values)
4015 {
4016 if (! method_exists($this->grammar, 'compileUpdateFrom')) {
4017 throw new LogicException('This database engine does not support the updateFrom method.');
4018 }
4019
4020 $this->applyBeforeQueryCallbacks();
4021
4022 $sql = $this->grammar->compileUpdateFrom($this, $values);
4023
4024 return $this->connection->update($sql, $this->cleanBindings(
4025 $this->grammar->prepareBindingsForUpdateFrom($this->bindings, $values)
4026 ));
4027 }
4028
4029 /**
4030 * Insert or update a record matching the attributes, and fill it with values.
4031 *
4032 * @param array $attributes
4033 * @param array $values
4034 * @return bool
4035 */
4036 public function updateOrInsert(array $attributes, $values = [])
4037 {
4038 $exists = $this->where($attributes)->exists();
4039
4040 if ($values instanceof Closure) {
4041 $values = $values($exists);
4042 }
4043
4044 if (! $exists) {
4045 return $this->insert(array_merge($attributes, $values));
4046 }
4047
4048 if (empty($values)) {
4049 return true;
4050 }
4051
4052 return (bool) $this->limit(1)->update($values);
4053 }
4054
4055 /**
4056 * Insert new records or update the existing ones.
4057 *
4058 * @param array $values
4059 * @param array|string $uniqueBy
4060 * @param array|null $update
4061 * @return int
4062 */
4063 public function upsert(array $values, $uniqueBy, $update = null)
4064 {
4065 if (empty($values)) {
4066 return 0;
4067 } elseif ($update === []) {
4068 return (int) $this->insert($values);
4069 }
4070
4071 if (! is_array(reset($values))) {
4072 $values = [$values];
4073 } else {
4074 foreach ($values as $key => $value) {
4075 ksort($value);
4076
4077 $values[$key] = $value;
4078 }
4079 }
4080
4081 if (is_null($update)) {
4082 $update = array_keys(reset($values));
4083 }
4084
4085 $this->applyBeforeQueryCallbacks();
4086
4087 $bindings = $this->cleanBindings(array_merge(
4088 Arr::flatten($values, 1),
4089 Helper::collect($update)->reject(function ($value, $key) {
4090 return is_int($key);
4091 })->all()
4092 ));
4093
4094 return $this->connection->affectingStatement(
4095 $this->grammar->compileUpsert($this, $values, (array) $uniqueBy, $update),
4096 $bindings
4097 );
4098 }
4099
4100 /**
4101 * Increment a column's value by a given amount.
4102 *
4103 * @param string $column
4104 * @param float|int $amount
4105 * @param array $extra
4106 * @return int
4107 *
4108 * @throws \InvalidArgumentException
4109 */
4110 public function increment($column, $amount = 1, array $extra = [])
4111 {
4112 if (! is_numeric($amount)) {
4113 throw new InvalidArgumentException('Non-numeric value passed to increment method.');
4114 }
4115
4116 return $this->incrementEach([$column => $amount], $extra);
4117 }
4118
4119 /**
4120 * Increment the given column's values by the given amounts.
4121 *
4122 * @param array<string, float|int|numeric-string> $columns
4123 * @param array<string, mixed> $extra
4124 * @return int
4125 *
4126 * @throws \InvalidArgumentException
4127 */
4128 public function incrementEach(array $columns, array $extra = [])
4129 {
4130 foreach ($columns as $column => $amount) {
4131 if (! is_numeric($amount)) {
4132 throw new InvalidArgumentException("Non-numeric value passed as increment amount for column: '$column'.");
4133 } elseif (! is_string($column)) {
4134 throw new InvalidArgumentException('Non-associative array passed to incrementEach method.');
4135 }
4136
4137 $columns[$column] = $this->raw("{$this->grammar->wrap($column)} + $amount");
4138 }
4139
4140 return $this->update(array_merge($columns, $extra));
4141 }
4142
4143 /**
4144 * Decrement a column's value by a given amount.
4145 *
4146 * @param string $column
4147 * @param float|int $amount
4148 * @param array $extra
4149 * @return int
4150 *
4151 * @throws \InvalidArgumentException
4152 */
4153 public function decrement($column, $amount = 1, array $extra = [])
4154 {
4155 if (! is_numeric($amount)) {
4156 throw new InvalidArgumentException('Non-numeric value passed to decrement method.');
4157 }
4158
4159 return $this->decrementEach([$column => $amount], $extra);
4160 }
4161
4162 /**
4163 * Decrement the given column's values by the given amounts.
4164 *
4165 * @param array<string, float|int|numeric-string> $columns
4166 * @param array<string, mixed> $extra
4167 * @return int
4168 *
4169 * @throws \InvalidArgumentException
4170 */
4171 public function decrementEach(array $columns, array $extra = [])
4172 {
4173 foreach ($columns as $column => $amount) {
4174 if (! is_numeric($amount)) {
4175 throw new InvalidArgumentException("Non-numeric value passed as decrement amount for column: '$column'.");
4176 } elseif (! is_string($column)) {
4177 throw new InvalidArgumentException('Non-associative array passed to decrementEach method.');
4178 }
4179
4180 $columns[$column] = $this->raw("{$this->grammar->wrap($column)} - $amount");
4181 }
4182
4183 return $this->update(array_merge($columns, $extra));
4184 }
4185
4186 /**
4187 * Delete records from the database.
4188 *
4189 * @param mixed $id
4190 * @return int
4191 */
4192 public function delete($id = null)
4193 {
4194 // If an ID is passed to the method, we will set the where clause to check the
4195 // ID to let developers to simply and quickly remove a single row from this
4196 // database without manually specifying the "where" clauses on the query.
4197 if (! is_null($id)) {
4198 $this->where($this->from.'.id', '=', $id);
4199 }
4200
4201 $this->applyBeforeQueryCallbacks();
4202
4203 return $this->connection->delete(
4204 $this->grammar->compileDelete($this), $this->cleanBindings(
4205 $this->grammar->prepareBindingsForDelete($this->bindings)
4206 )
4207 );
4208 }
4209
4210 /**
4211 * Run a truncate statement on the table.
4212 *
4213 * @return void
4214 */
4215 public function truncate()
4216 {
4217 $this->applyBeforeQueryCallbacks();
4218
4219 foreach ($this->grammar->compileTruncate($this) as $sql => $bindings) {
4220 $this->connection->statement($sql, $bindings);
4221 }
4222 }
4223
4224 /**
4225 * Get a new instance of the query builder.
4226 *
4227 * @return \FluentBoards\Framework\Database\Query\Builder
4228 */
4229 public function newQuery()
4230 {
4231 return new static($this->connection, $this->grammar, $this->processor);
4232 }
4233
4234 /**
4235 * Create a new query instance for a sub-query.
4236 *
4237 * @return \FluentBoards\Framework\Database\Query\Builder
4238 */
4239 protected function forSubQuery()
4240 {
4241 return $this->newQuery();
4242 }
4243
4244 /**
4245 * Get all of the query builder's columns in a text-only array with all expressions evaluated.
4246 *
4247 * @return array
4248 */
4249 public function getColumns()
4250 {
4251 return ! is_null($this->columns)
4252 ? array_map(fn ($column) => $this->grammar->getValue($column), $this->columns)
4253 : [];
4254 }
4255
4256 /**
4257 * Create a raw database expression.
4258 *
4259 * @param mixed $value
4260 * @return \FluentBoards\Framework\Database\Query\Expression
4261 */
4262 public function raw($value)
4263 {
4264 return $this->connection->raw($value);
4265 }
4266
4267 /**
4268 * Get the query builder instances that are used in the union of the query.
4269 *
4270 * @return \FluentBoards\Framework\Support\Collection
4271 */
4272 protected function getUnionBuilders()
4273 {
4274 return isset($this->unions)
4275 ? Helper::collect($this->unions)->pluck('query')
4276 : Helper::collect();
4277 }
4278
4279 /**
4280 * Get the current query value bindings in a flattened array.
4281 *
4282 * @return array
4283 */
4284 public function getBindings()
4285 {
4286 return Arr::flatten($this->bindings);
4287 }
4288
4289 /**
4290 * Get the raw array of bindings.
4291 *
4292 * @return array
4293 */
4294 public function getRawBindings()
4295 {
4296 return $this->bindings;
4297 }
4298
4299 /**
4300 * Set the bindings on the query builder.
4301 *
4302 * @param array $bindings
4303 * @param string $type
4304 * @return $this
4305 *
4306 * @throws \InvalidArgumentException
4307 */
4308 public function setBindings(array $bindings, $type = 'where')
4309 {
4310 if (! array_key_exists($type, $this->bindings)) {
4311 throw new InvalidArgumentException("Invalid binding type: {$type}.");
4312 }
4313
4314 $this->bindings[$type] = $bindings;
4315
4316 return $this;
4317 }
4318
4319 /**
4320 * Add a binding to the query.
4321 *
4322 * @param mixed $value
4323 * @param string $type
4324 * @return $this
4325 *
4326 * @throws \InvalidArgumentException
4327 */
4328 public function addBinding($value, $type = 'where')
4329 {
4330 if (! array_key_exists($type, $this->bindings)) {
4331 throw new InvalidArgumentException("Invalid binding type: {$type}.");
4332 }
4333
4334 if (is_array($value)) {
4335 $this->bindings[$type] = array_values(array_map(
4336 [$this, 'castBinding'],
4337 array_merge($this->bindings[$type], $value),
4338 ));
4339 } else {
4340 $this->bindings[$type][] = $this->castBinding($value);
4341 }
4342
4343 return $this;
4344 }
4345
4346 /**
4347 * Cast the given binding value.
4348 *
4349 * @param mixed $value
4350 * @return mixed
4351 */
4352 public function castBinding($value)
4353 {
4354 if (function_exists('enum_exists')) {
4355 if ($value instanceof \BackedEnum) {
4356 return $value->value;
4357 }
4358 }
4359
4360 return $value;
4361 }
4362
4363 /**
4364 * Merge an array of bindings into our bindings.
4365 *
4366 * @param \FluentBoards\Framework\Database\Query\Builder $query
4367 * @return $this
4368 */
4369 public function mergeBindings(self $query)
4370 {
4371 $this->bindings = array_merge_recursive($this->bindings, $query->bindings);
4372
4373 return $this;
4374 }
4375
4376 /**
4377 * Remove all of the expressions from a list of bindings.
4378 *
4379 * @param array $bindings
4380 * @return array
4381 */
4382 public function cleanBindings(array $bindings)
4383 {
4384 return Helper::collect($bindings)
4385 ->reject(function ($binding) {
4386 return $binding instanceof Expression;
4387 })
4388 ->map([$this, 'castBinding'])
4389 ->values()
4390 ->all();
4391 }
4392
4393 /**
4394 * Get a scalar type value from an unknown type of input.
4395 *
4396 * @param mixed $value
4397 * @return mixed
4398 */
4399 protected function flattenValue($value)
4400 {
4401 return is_array($value) ? Helper::head(Arr::flatten($value)) : $value;
4402 }
4403
4404 /**
4405 * Get the default key name of the table.
4406 *
4407 * @return string
4408 */
4409 protected function defaultKeyName()
4410 {
4411 return 'id';
4412 }
4413
4414 /**
4415 * Get the database connection instance.
4416 *
4417 * @return \FluentBoards\Framework\Database\ConnectionInterface
4418 */
4419 public function getConnection()
4420 {
4421 return $this->connection;
4422 }
4423
4424 /**
4425 * Get the database query processor instance.
4426 *
4427 * @return \FluentBoards\Framework\Database\Query\Processors\Processor
4428 */
4429 public function getProcessor()
4430 {
4431 return $this->processor;
4432 }
4433
4434 /**
4435 * Get the query grammar instance.
4436 *
4437 * @return \FluentBoards\Framework\Database\Query\Grammars\Grammar
4438 */
4439 public function getGrammar()
4440 {
4441 return $this->grammar;
4442 }
4443
4444 /**
4445 * Use the write pdo for query.
4446 *
4447 * @return $this
4448 */
4449 public function useWritePdo()
4450 {
4451 $this->useWritePdo = true;
4452
4453 return $this;
4454 }
4455
4456 /**
4457 * Determine if the value is a query builder instance or a Closure.
4458 *
4459 * @param mixed $value
4460 * @return bool
4461 */
4462 protected function isQueryable($value)
4463 {
4464 return $value instanceof self ||
4465 $value instanceof OrmBuilder ||
4466 $value instanceof Relation ||
4467 $value instanceof Closure;
4468 }
4469
4470 /**
4471 * Clone the query.
4472 *
4473 * @return static
4474 */
4475 public function clone()
4476 {
4477 return clone $this;
4478 }
4479
4480 /**
4481 * Clone the query without the given properties.
4482 *
4483 * @param array $properties
4484 * @return static
4485 */
4486 public function cloneWithout(array $properties)
4487 {
4488 return Helper::tap($this->clone(), function ($clone) use ($properties) {
4489 foreach ($properties as $property) {
4490 $clone->{$property} = null;
4491 }
4492 });
4493 }
4494
4495 /**
4496 * Clone the query without the given bindings.
4497 *
4498 * @param array $except
4499 * @return static
4500 */
4501 public function cloneWithoutBindings(array $except)
4502 {
4503 return Helper::tap($this->clone(), function ($clone) use ($except) {
4504 foreach ($except as $type) {
4505 $clone->bindings[$type] = [];
4506 }
4507 });
4508 }
4509
4510 /**
4511 * Handle dynamic method calls into the method.
4512 *
4513 * @param string $method
4514 * @param array $parameters
4515 * @return mixed
4516 *
4517 * @throws \BadMethodCallException
4518 */
4519 public function __call($method, $parameters)
4520 {
4521 if (static::hasMacro($method)) {
4522 return $this->macroCall($method, $parameters);
4523 }
4524
4525 if (Str::startsWith($method, 'where')) {
4526 return $this->dynamicWhere($method, $parameters);
4527 }
4528
4529 static::throwBadMethodCallException($method);
4530 }
4531
4532 /**
4533 * Set a dynamic property.
4534 *
4535 * @param string $key
4536 * @param mixed $value
4537 */
4538 public function __set($key, $value)
4539 {
4540 $this->dynamicProperties[$key] = $value;
4541 }
4542
4543 /**
4544 * Get dynamically injected value.
4545 *
4546 * @param string $key
4547 * @return mixed
4548 */
4549 public function __get($key)
4550 {
4551 return $this->dynamicProperties[$key] ?? null;
4552 }
4553 }
4554