PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.41
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.41
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 1.41, at vendor/wpfluent/framework/src/WPFluent/Database/Query/Builder.php

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