PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.7
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.7
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / vendor / wpfluent / framework / src / WPFluent / Database / Query / Builder.php

Builder.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.7, at vendor/wpfluent/framework/src/WPFluent/Database/Query/Builder.php

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