PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 1.0.94
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v1.0.94
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 1.1.0 All 77 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 1.0.94, at vendor/wpfluent/framework/src/WPFluent/Database/Query/Builder.php

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