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

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

2,164 lines 60.6 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\Orm;
4
5 use Closure;
6 use Exception;
7 use ReflectionClass;
8 use ReflectionMethod;
9 use BadMethodCallException;
10 use FluentCommunity\Framework\Support\Arr;
11 use FluentCommunity\Framework\Support\Str;
12 use FluentCommunity\Framework\Support\Helper;
13 use FluentCommunity\Framework\Pagination\Paginator;
14 use FluentCommunity\Framework\Support\ForwardsCalls;
15 use FluentCommunity\Framework\Support\ArrayableInterface;
16 use FluentCommunity\Framework\Database\Query\Expression;
17 use FluentCommunity\Framework\Database\Concerns\BuildsQueries;
18 use FluentCommunity\Framework\Database\RecordsNotFoundException;
19 use FluentCommunity\Framework\Database\Orm\Relations\Relation;
20 use FluentCommunity\Framework\Database\Orm\Relations\BelongsToMany;
21 use FluentCommunity\Framework\Database\Orm\RelationNotFoundException;
22 use FluentCommunity\Framework\Database\Orm\Concerns\QueriesRelationships;
23 use FluentCommunity\Framework\Database\UniqueConstraintViolationException;
24 use FluentCommunity\Framework\Database\Query\Builder as QueryBuilder;
25
26 /**
27 * @property-read HigherOrderBuilderProxy $orWhere
28 *
29 * @mixin \FluentCommunity\Framework\Database\Query\Builder
30 */
31 class Builder
32 {
33 use BuildsQueries, ForwardsCalls, QueriesRelationships {
34 BuildsQueries::sole as baseSole;
35 }
36
37 /**
38 * The base query builder instance.
39 *
40 * @var \FluentCommunity\Framework\Database\Query\Builder
41 */
42 protected $query;
43
44 /**
45 * The model being queried.
46 *
47 * @var \FluentCommunity\Framework\Database\Orm\Model
48 */
49 protected $model;
50
51 /**
52 * The relationships that should be eager loaded.
53 *
54 * @var array
55 */
56 protected $eagerLoad = [];
57
58 /**
59 * All of the globally registered builder macros.
60 *
61 * @var array
62 */
63 protected static $macros = [];
64
65 /**
66 * All of the locally registered builder macros.
67 *
68 * @var array
69 */
70 protected $localMacros = [];
71
72 /**
73 * A replacement for the typical delete function.
74 *
75 * @var \Closure
76 */
77 protected $onDelete;
78
79 /**
80 * The properties that should be returned from query builder.
81 *
82 * @var string[]
83 */
84 protected $propertyPassthru = [
85 'from',
86 ];
87
88 /**
89 * The methods that should be returned from query builder.
90 *
91 * @var string[]
92 */
93 protected $passthru = [
94 'aggregate',
95 'average',
96 'avg',
97 'count',
98 'dd',
99 'ddrawsql',
100 'doesntexist',
101 'doesntexistor',
102 'dump',
103 'dumprawsql',
104 'exists',
105 'existsor',
106 'explain',
107 'getbindings',
108 'getconnection',
109 'getgrammar',
110 'getrawbindings',
111 'implode',
112 'insert',
113 'insertgetid',
114 'insertorignore',
115 'insertusing',
116 'insertorignoreusing',
117 'max',
118 'min',
119 'raw',
120 'rawvalue',
121 'sum',
122 'tosql',
123 'torawsql',
124 ];
125
126 /**
127 * Applied global scopes.
128 *
129 * @var array
130 */
131 protected $scopes = [];
132
133
134 /**
135 * Applied model appends.
136 *
137 * @var array
138 */
139 protected $appends = [];
140
141 /**
142 * Applied model hidden.
143 *
144 * @var array
145 */
146 protected $hidden = [];
147
148 /**
149 * Removed global scopes.
150 *
151 * @var array
152 */
153 protected $removedScopes = [];
154
155 /**
156 * The callbacks that should be invoked after retrieving data from the database.
157 *
158 * @var array
159 */
160 protected $afterQueryCallbacks = [];
161
162 /**
163 * Create a new Orm query builder instance.
164 *
165 * @param \FluentCommunity\Framework\Database\Query\Builder $query
166 * @return void
167 */
168 public function __construct(QueryBuilder $query)
169 {
170 $this->query = $query;
171 }
172
173 /**
174 * Create and return an un-saved model instance.
175 *
176 * @param array $attributes
177 * @return \FluentCommunity\Framework\Database\Orm\Model|static
178 */
179 public function make(array $attributes = [])
180 {
181 return $this->newModelInstance($attributes);
182 }
183
184 /**
185 * Register a new global scope.
186 *
187 * @param string $identifier
188 * @param \FluentCommunity\Framework\Database\Orm\Scope|\Closure $scope
189 * @return $this
190 */
191 public function withGlobalScope($identifier, $scope)
192 {
193 $this->scopes[$identifier] = $scope;
194
195 if (method_exists($scope, 'extend')) {
196 $scope->extend($this);
197 }
198
199 return $this;
200 }
201
202 /**
203 * Remove a registered global scope.
204 *
205 * @param \FluentCommunity\Framework\Database\Orm\Scope|string $scope
206 * @return $this
207 */
208 public function withoutGlobalScope($scope)
209 {
210 if (! is_string($scope)) {
211 $scope = get_class($scope);
212 }
213
214 unset($this->scopes[$scope]);
215
216 $this->removedScopes[] = $scope;
217
218 return $this;
219 }
220
221 /**
222 * Remove all or passed registered global scopes.
223 *
224 * @param array|null $scopes
225 * @return $this
226 */
227 public function withoutGlobalScopes(array $scopes = null)
228 {
229 if (! is_array($scopes)) {
230 $scopes = array_keys($this->scopes);
231 }
232
233 foreach ($scopes as $scope) {
234 $this->withoutGlobalScope($scope);
235 }
236
237 return $this;
238 }
239
240 /**
241 * Register model appends.
242 * @param array $appends
243 * @return $this
244 */
245 public function addAppends(array $appends)
246 {
247 $this->appends = array_merge($this->appends, $appends);
248 return $this;
249 }
250
251 /**
252 * Register model hidden.
253 * @param array $hidden
254 * @return $this
255 */
256 public function addHidden(array $hidden)
257 {
258 $this->hidden = array_merge($this->hidden, $hidden);
259 return $this;
260 }
261
262 /**
263 * Get an array of global scopes that were removed from the query.
264 *
265 * @return array
266 */
267 public function removedScopes()
268 {
269 return $this->removedScopes;
270 }
271
272 /**
273 * Add a where clause on the primary key to the query.
274 *
275 * @param mixed $id
276 * @return $this
277 */
278 public function whereKey($id)
279 {
280 if ($id instanceof Model) {
281 $id = $id->getKey();
282 }
283
284 if (is_array($id) || $id instanceof ArrayableInterface) {
285 if (in_array($this->model->getKeyType(), ['int', 'integer'])) {
286 $this->query->whereIntegerInRaw($this->model->getQualifiedKeyName(), $id);
287 } else {
288 $this->query->whereIn($this->model->getQualifiedKeyName(), $id);
289 }
290
291 return $this;
292 }
293
294 if ($id !== null && $this->model->getKeyType() === 'string') {
295 $id = (string) $id;
296 }
297
298 return $this->where($this->model->getQualifiedKeyName(), '=', $id);
299 }
300
301 /**
302 * Add a where clause on the primary key to the query.
303 *
304 * @param mixed $id
305 * @return $this
306 */
307 public function whereKeyNot($id)
308 {
309 if ($id instanceof Model) {
310 $id = $id->getKey();
311 }
312
313 if (is_array($id) || $id instanceof ArrayableInterface) {
314 if (in_array($this->model->getKeyType(), ['int', 'integer'])) {
315 $this->query->whereIntegerNotInRaw($this->model->getQualifiedKeyName(), $id);
316 } else {
317 $this->query->whereNotIn($this->model->getQualifiedKeyName(), $id);
318 }
319
320 return $this;
321 }
322
323 if ($id !== null && $this->model->getKeyType() === 'string') {
324 $id = (string) $id;
325 }
326
327 return $this->where($this->model->getQualifiedKeyName(), '!=', $id);
328 }
329
330 /**
331 * Add a basic where clause to the query.
332 *
333 * @param \Closure|string|array|\FluentCommunity\Framework\Database\Query\Expression $column
334 * @param mixed $operator
335 * @param mixed $value
336 * @param string $boolean
337 * @return $this
338 */
339 public function where($column, $operator = null, $value = null, $boolean = 'and')
340 {
341 if ($column instanceof Closure && is_null($operator)) {
342 $column($query = $this->model->newQueryWithoutRelationships());
343
344 $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
345 } else {
346 $this->query->where(...func_get_args());
347 }
348
349 return $this;
350 }
351
352 /**
353 * Add a basic where clause to the query, and return the first result.
354 *
355 * @param \Closure|string|array|\FluentCommunity\Framework\Database\Query\Expression $column
356 * @param mixed $operator
357 * @param mixed $value
358 * @param string $boolean
359 * @return \FluentCommunity\Framework\Database\Orm\Model|static|null
360 */
361 public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
362 {
363 return $this->where(...func_get_args())->first();
364 }
365
366 /**
367 * Add an "or where" clause to the query.
368 *
369 * @param \Closure|array|string|\FluentCommunity\Framework\Database\Query\Expression $column
370 * @param mixed $operator
371 * @param mixed $value
372 * @return $this
373 */
374 public function orWhere($column, $operator = null, $value = null)
375 {
376 [$value, $operator] = $this->query->prepareValueAndOperator(
377 $value, $operator, func_num_args() === 2
378 );
379
380 return $this->where($column, $operator, $value, 'or');
381 }
382
383 /**
384 * Add a basic "where not" clause to the query.
385 *
386 * @param (\Closure(static): mixed)|string|array|\FluentCommunity\Framework\Database\Query\Expression $column
387 * @param mixed $operator
388 * @param mixed $value
389 * @param string $boolean
390 * @return $this
391 */
392 public function whereNot($column, $operator = null, $value = null, $boolean = 'and')
393 {
394 return $this->where($column, $operator, $value, $boolean.' not');
395 }
396
397 /**
398 * Add an "or where not" clause to the query.
399 *
400 * @param (\Closure(static): mixed)|array|string|\FluentCommunity\Framework\Database\Query\Expression $column
401 * @param mixed $operator
402 * @param mixed $value
403 * @return $this
404 */
405 public function orWhereNot($column, $operator = null, $value = null)
406 {
407 return $this->whereNot($column, $operator, $value, 'or');
408 }
409
410 /**
411 * Add an "order by" clause for a timestamp to the query.
412 *
413 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
414 * @return $this
415 */
416 public function latest($column = null)
417 {
418 if (is_null($column)) {
419 $column = $this->model->getCreatedAtColumn() ?? 'created_at';
420 }
421
422 $this->query->latest($column);
423
424 return $this;
425 }
426
427 /**
428 * Add an "order by" clause for a timestamp to the query.
429 *
430 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
431 * @return $this
432 */
433 public function oldest($column = null)
434 {
435 if (is_null($column)) {
436 $column = $this->model->getCreatedAtColumn() ?? 'created_at';
437 }
438
439 $this->query->oldest($column);
440
441 return $this;
442 }
443
444 /**
445 * Create a collection of models from plain arrays.
446 *
447 * @param array $items
448 * @return \FluentCommunity\Framework\Database\Orm\Collection
449 */
450 public function hydrate(array $items, $args = [])
451 {
452 $instance = $this->newModelInstance();
453
454 return $instance->newCollection(array_map(function ($item) use ($items, $instance, $args) {
455 $model = $instance->newFromBuilder($item, null, $args);
456
457 if (count($items) > 1) {
458 $model->preventsLazyLoading = Model::preventsLazyLoading();
459 }
460
461 return $model;
462 }, $items));
463 }
464
465 /**
466 * Create a collection of models from a raw query.
467 *
468 * @param string $query
469 * @param array $bindings
470 * @return \FluentCommunity\Framework\Database\Orm\Collection
471 */
472 public function fromQuery($query, $bindings = [])
473 {
474 return $this->hydrate(
475 $this->query->getConnection()->select($query, $bindings)
476 );
477 }
478
479 /**
480 * Find a model by its primary key.
481 *
482 * @param mixed $id
483 * @param array $columns
484 * @return \FluentCommunity\Framework\Database\Orm\Model|\FluentCommunity\Framework\Database\Orm\Collection|static[]|static|null
485 */
486 public function find($id, $columns = ['*'])
487 {
488 if (is_array($id) || $id instanceof ArrayableInterface) {
489 return $this->findMany($id, $columns);
490 }
491
492 return $this->whereKey($id)->first($columns);
493 }
494
495 /**
496 * Find multiple models by their primary keys.
497 *
498 * @param \FluentCommunity\Framework\Support\ArrayableInterface|array $ids
499 * @param array $columns
500 * @return \FluentCommunity\Framework\Database\Orm\Collection
501 */
502 public function findMany($ids, $columns = ['*'])
503 {
504 $ids = $ids instanceof ArrayableInterface ? $ids->toArray() : $ids;
505
506 if (empty($ids)) {
507 return $this->model->newCollection();
508 }
509
510 return $this->whereKey($ids)->get($columns);
511 }
512
513 /**
514 * Find a model by its primary key or throw an exception.
515 *
516 * @param mixed $id
517 * @param array $columns
518 * @return \FluentCommunity\Framework\Database\Orm\Model|\FluentCommunity\Framework\Database\Orm\Collection|static|static[]
519 *
520 * @throws \FluentCommunity\Framework\Database\Orm\ModelNotFoundException
521 */
522 public function findOrFail($id, $columns = ['*'])
523 {
524 $result = $this->find($id, $columns);
525
526 $id = $id instanceof Arrayable ? $id->toArray() : $id;
527
528 if (is_array($id)) {
529 if (count($result) !== count(array_unique($id))) {
530 throw (new ModelNotFoundException)->setModel(
531 get_class($this->model), array_diff($id, $result->modelKeys())
532 );
533 }
534
535 return $result;
536 }
537
538 if (is_null($result)) {
539 throw (new ModelNotFoundException)->setModel(
540 get_class($this->model), $id
541 );
542 }
543
544 return $result;
545 }
546
547 /**
548 * Find a model by its primary key or return fresh model instance.
549 *
550 * @param mixed $id
551 * @param array $columns
552 * @return \FluentCommunity\Framework\Database\Orm\Model|static
553 */
554 public function findOrNew($id, $columns = ['*'])
555 {
556 if (! is_null($model = $this->find($id, $columns))) {
557 return $model;
558 }
559
560 return $this->newModelInstance();
561 }
562
563 /**
564 * Find a model by its primary key or call a callback.
565 *
566 * @template TValue
567 *
568 * @param mixed $id
569 * @param (\Closure(): TValue)|list<string>|string $columns
570 * @param (\Closure(): TValue)|null $callback
571 * @return (
572 * $id is (\FluentCommunity\Framework\Support\ArrayableInterface<array-key, mixed>|array<mixed>)
573 * ? \FluentCommunity\Framework\Database\Orm\Collection<int, TModel>
574 * : TModel|TValue
575 * )
576 */
577 public function findOr($id, $columns = ['*'], Closure $callback = null)
578 {
579 if ($columns instanceof Closure) {
580 $callback = $columns;
581
582 $columns = ['*'];
583 }
584
585 if (! is_null($model = $this->find($id, $columns))) {
586 return $model;
587 }
588
589 return $callback();
590 }
591
592 /**
593 * Get the first record matching the attributes or instantiate it.
594 *
595 * @param array $attributes
596 * @param array $values
597 * @return \FluentCommunity\Framework\Database\Orm\Model|static
598 */
599 public function firstOrNew(array $attributes = [], array $values = [])
600 {
601 if (! is_null($instance = $this->where($attributes)->first())) {
602 return $instance;
603 }
604
605 return $this->newModelInstance(array_merge($attributes, $values));
606 }
607
608 /**
609 * Get the first record matching the attributes or create it.
610 *
611 * @param array $attributes
612 * @param array $values
613 * @return \FluentCommunity\Framework\Database\Orm\Model|static
614 */
615 public function firstOrCreate(array $attributes = [], array $values = [])
616 {
617 if (! is_null($instance = (clone $this)->where($attributes)->first())) {
618 return $instance;
619 }
620
621 return $this->createOrFirst($attributes, $values);
622 }
623
624 /**
625 * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
626 *
627 * @param array $attributes
628 * @param array $values
629 * @return TModel
630 */
631 public function createOrFirst(array $attributes = [], array $values = [])
632 {
633 try {
634 return $this->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, $values)));
635 } catch (UniqueConstraintViolationException $e) {
636 if ($instance = $this->useWritePdo()->where($attributes)->first()) {
637 return $instance;
638 } throw $e;
639 }
640 }
641
642 /**
643 * Create or update a record matching the attributes, and fill it with values.
644 *
645 * @param array $attributes
646 * @param array $values
647 * @return \FluentCommunity\Framework\Database\Orm\Model|static
648 */
649 public function updateOrCreate(array $attributes, array $values = [])
650 {
651 return Helper::tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
652 if (! $instance->wasRecentlyCreated) {
653 $instance->fill($values)->save();
654 }
655 });
656 }
657
658 /**
659 * Execute the query and get the first result or throw an exception.
660 *
661 * @param array $columns
662 * @return \FluentCommunity\Framework\Database\Orm\Model|static
663 *
664 * @throws \FluentCommunity\Framework\Database\Orm\ModelNotFoundException
665 */
666 public function firstOrFail($columns = ['*'])
667 {
668 if (! is_null($model = $this->first($columns))) {
669 return $model;
670 }
671
672 throw (new ModelNotFoundException)->setModel(get_class($this->model));
673 }
674
675 /**
676 * Execute the query and get the first result or call a callback.
677 *
678 * @param \Closure|array $columns
679 * @param \Closure|null $callback
680 * @return \FluentCommunity\Framework\Database\Orm\Model|static|mixed
681 */
682 public function firstOr($columns = ['*'], Closure $callback = null)
683 {
684 if ($columns instanceof Closure) {
685 $callback = $columns;
686
687 $columns = ['*'];
688 }
689
690 if (! is_null($model = $this->first($columns))) {
691 return $model;
692 }
693
694 return $callback();
695 }
696
697 /**
698 * Execute the query and get the first result if it's the sole matching record.
699 *
700 * @param array|string $columns
701 * @return \FluentCommunity\Framework\Database\Orm\Model
702 *
703 * @throws \FluentCommunity\Framework\Database\Orm\ModelNotFoundException
704 * @throws \FluentCommunity\Framework\Database\MultipleRecordsFoundException
705 */
706 public function sole($columns = ['*'])
707 {
708 try {
709 return $this->baseSole($columns);
710 } catch (RecordsNotFoundException $exception) {
711 throw (new ModelNotFoundException)->setModel(get_class($this->model));
712 }
713 }
714
715 /**
716 * Get a single column's value from the first result of a query.
717 *
718 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
719 * @return mixed
720 */
721 public function value($column)
722 {
723 if ($result = $this->first([$column])) {
724 $column = $column instanceof Expression ? $column->getValue(
725 $this->getGrammar()
726 ) : $column;
727
728 return $result->{Str::afterLast($column, '.')};
729 }
730 }
731
732 /**
733 * Get a single column's value from the first result of a query if it's the sole matching record.
734 *
735 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
736 * @return mixed
737 *
738 * @throws \FluentCommunity\Framework\Database\Orm\ModelNotFoundException<TModel>
739 * @throws \FluentCommunity\Framework\Database\MultipleRecordsFoundException
740 */
741 public function soleValue($column)
742 {
743 $column = $column instanceof Expression ? $column->getValue(
744 $this->getGrammar()
745 ) : $column;
746
747 return $this->sole([$column])->{Str::afterLast($column, '.')};
748 }
749
750 /**
751 * Get a single column's value from the first result of the query or throw an exception.
752 *
753 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
754 * @return mixed
755 *
756 * @throws \FluentCommunity\Framework\Database\Orm\ModelNotFoundException
757 */
758 public function valueOrFail($column)
759 {
760 $column = $column instanceof Expression ? $column->getValue(
761 $this->getGrammar()
762 ) : $column;
763
764 return $this->firstOrFail([$column])->{Str::afterLast($column, '.')};
765 }
766
767 /**
768 * Execute the query as a "select" statement.
769 *
770 * @param array|string $columns
771 * @return \FluentCommunity\Framework\Database\Orm\Collection|static[]
772 */
773 public function get($columns = ['*'])
774 {
775 $builder = $this->applyScopes();
776
777 // If we actually found models we will also eager load any relationships that
778 // have been specified as needing to be eager loaded, which will solve the
779 // n+1 query issue for the developers to avoid running a lot of queries.
780 if (count($models = $builder->getModels($columns)) > 0) {
781 $models = $builder->eagerLoadRelations($models);
782 }
783
784 return $this->applyAfterQueryCallbacks(
785 $builder->getModel()->newCollection($models)
786 );
787 }
788
789 /**
790 * Get the hydrated models without eager loading.
791 *
792 * @param array|string $columns
793 * @return \FluentCommunity\Framework\Database\Orm\Model[]|static[]
794 */
795 public function getModels($columns = ['*'])
796 {
797 return $this->model->hydrate(
798 $this->query->get($columns)->all(),
799 [
800 'appends' => $this->appends,
801 'hidden' => $this->hidden,
802 ]
803 )->all();
804 }
805
806 /**
807 * Eager load the relationships for the models.
808 *
809 * @param array $models
810 * @return array
811 */
812 public function eagerLoadRelations(array $models)
813 {
814 foreach ($this->eagerLoad as $name => $constraints) {
815 // For nested eager loads we'll skip loading them here and they will
816 // be set as an eager load on the query to retrieve the relation
817 // so that they will be eager loaded on that query, because
818 // that is where they get hydrated as models.
819 if (! str_contains($name, '.')) {
820 $models = $this->eagerLoadRelation($models, $name, $constraints);
821 }
822 }
823
824 return $models;
825 }
826
827 /**
828 * Eagerly load the relationship on a set of models.
829 *
830 * @param array $models
831 * @param string $name
832 * @param \Closure $constraints
833 * @return array
834 */
835 protected function eagerLoadRelation(array $models, $name, Closure $constraints)
836 {
837 // First we will "back up" the existing where conditions on the query so we can
838 // add our eager constraints. Then we will merge the wheres that were on the
839 // query back to it in order that any where conditions might be specified.
840 $relation = $this->getRelation($name);
841
842 $relation->addEagerConstraints($models);
843
844 $constraints($relation);
845
846 // Once we have the results, we just match those back up to their parent models
847 // using the relationship instance. Then we just return the finished arrays
848 // of models which have been eagerly hydrated and are readied for return.
849 return $relation->match(
850 $relation->initRelation($models, $name),
851 $relation->getEager(), $name
852 );
853 }
854
855 /**
856 * Get the relation instance for the given relation name.
857 *
858 * @param string $name
859 * @return \FluentCommunity\Framework\Database\Orm\Relations\Relation
860 */
861 public function getRelation($name)
862 {
863 // We want to run a relationship query without any constrains so that we will
864 // not have to remove these where clauses manually which gets really hacky
865 // and error prone. We don't want constraints because we add eager ones.
866 $relation = Relation::noConstraints(function () use ($name) {
867 try {
868 return $this->getModel()->newInstance()->$name();
869 } catch (BadMethodCallException $e) {
870 throw RelationNotFoundException::make($this->getModel(), $name);
871 }
872 });
873
874 $nested = $this->relationsNestedUnder($name);
875
876 // If there are nested relationships set on the query, we will put those onto
877 // the query instances so that they can be handled after this relationship
878 // is loaded. In this way they will all trickle down as they are loaded.
879 if (count($nested) > 0) {
880 $relation->getQuery()->with($nested);
881 }
882
883 return $relation;
884 }
885
886 /**
887 * Get the deeply nested relations for a given top-level relation.
888 *
889 * @param string $relation
890 * @return array
891 */
892 protected function relationsNestedUnder($relation)
893 {
894 $nested = [];
895
896 // We are basically looking for any relationships that are nested deeper than
897 // the given top-level relationship. We will just check for any relations
898 // that start with the given top relations and adds them to our arrays.
899 foreach ($this->eagerLoad as $name => $constraints) {
900 if ($this->isNestedUnder($relation, $name)) {
901 $nested[substr($name, strlen($relation.'.'))] = $constraints;
902 }
903 }
904
905 return $nested;
906 }
907
908 /**
909 * Determine if the relationship is nested.
910 *
911 * @param string $relation
912 * @param string $name
913 * @return bool
914 */
915 protected function isNestedUnder($relation, $name)
916 {
917 return Str::contains($name, '.') && Str::startsWith($name, $relation.'.');
918 }
919
920 /**
921 * Register a closure to be invoked after the query is executed.
922 *
923 * @param \Closure $callback
924 * @return $this
925 */
926 public function afterQuery(Closure $callback)
927 {
928 $this->afterQueryCallbacks[] = $callback;
929
930 return $this;
931 }
932
933 /**
934 * Invoke the "after query" modification callbacks.
935 *
936 * @param mixed $result
937 * @return mixed
938 */
939 public function applyAfterQueryCallbacks($result)
940 {
941 foreach ($this->afterQueryCallbacks as $afterQueryCallback) {
942 $result = $afterQueryCallback($result) ?: $result;
943 }
944
945 return $result;
946 }
947
948 /**
949 * Get a lazy collection for the given query.
950 *
951 * @return \FluentCommunity\Framework\Support\LazyCollection
952 */
953 public function cursor()
954 {
955 return $this->applyScopes()->query->cursor()->map(function ($record) {
956 $model = $this->newModelInstance()->newFromBuilder($record);
957
958 return $this->applyAfterQueryCallbacks($this->newModelInstance()->newCollection([$model]))->first();
959 })->reject(fn ($model) => is_null($model));
960 }
961
962 /**
963 * Add a generic "order by" clause if the query doesn't already have one.
964 *
965 * @return void
966 */
967 protected function enforceOrderBy()
968 {
969 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
970 $this->orderBy($this->model->getQualifiedKeyName(), 'asc');
971 }
972 }
973
974 /**
975 * Get an array with the values of a given column.
976 *
977 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
978 * @param string|null $key
979 * @return \FluentCommunity\Framework\Support\Collection
980 */
981 public function pluck($column, $key = null)
982 {
983 $results = $this->toBase()->pluck($column, $key);
984
985 $column = $column instanceof Expression ? $column->getValue(
986 $this->getGrammar()
987 ) : $column;
988
989 $column = Str::after($column, "{$this->model->getTable()}.");
990
991 // If the model has a mutator for the requested column, we will spin through
992 // the results and mutate the values so that the mutated version of these
993 // columns are returned as you would expect from these Eloquent models.
994 if (! $this->model->hasGetMutator($column) &&
995 ! $this->model->hasCast($column) &&
996 ! in_array($column, $this->model->getDates())) {
997 return $results;
998 }
999
1000 return $this->applyAfterQueryCallbacks(
1001 $results->map(function ($value) use ($column) {
1002 return $this->model->newFromBuilder([$column => $value])->{$column};
1003 })
1004 );
1005 }
1006
1007 /**
1008 * Paginate the given query.
1009 *
1010 * @param int|null|\Closure $perPage
1011 * @param array|string $columns
1012 * @param string $pageName
1013 * @param int|null $page
1014 * @param \Closure|int|null $total
1015 * @return \FluentCommunity\Framework\Pagination\LengthAwarePaginator
1016 *
1017 * @throws \InvalidArgumentException
1018 */
1019 public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)
1020 {
1021 $page = $page ?: Paginator::resolveCurrentPage($pageName);
1022
1023 $total = Helper::value($total) ?? $this->toBase()->getCountForPagination();
1024
1025 $perPage = ($perPage instanceof Closure
1026 ? $perPage($total)
1027 : $perPage
1028 ) ?: $this->model->getPerPage();
1029
1030 $results = $total
1031 ? $this->forPage($page, $perPage)->get($columns)
1032 : $this->model->newCollection();
1033
1034 return $this->paginator($results, $total, $perPage, $page, [
1035 'path' => Paginator::resolveCurrentPath(),
1036 'pageName' => $pageName,
1037 ]);
1038 }
1039
1040 /**
1041 * Paginate the given query into a simple paginator.
1042 *
1043 * @param int|null $perPage
1044 * @param array $columns
1045 * @param string $pageName
1046 * @param int|null $page
1047 * @return \FluentCommunity\Framework\Pagination\PaginatorInterface
1048 */
1049 public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
1050 {
1051 $page = $page ?: Paginator::resolveCurrentPage($pageName);
1052
1053 $perPage = $perPage ?: $this->model->getPerPage();
1054
1055 // Next we will set the limit and offset for this query so that when we get the
1056 // results we get the proper section of results. Then, we'll create the full
1057 // paginator instances for these results with the given page and per page.
1058 $this->skip(($page - 1) * $perPage)->take($perPage + 1);
1059
1060 return $this->simplePaginator($this->get($columns), $perPage, $page, [
1061 'path' => Paginator::resolveCurrentPath(),
1062 'pageName' => $pageName,
1063 ]);
1064 }
1065
1066 /**
1067 * Paginate the given query into a cursor paginator.
1068 *
1069 * @param int|null $perPage
1070 * @param array $columns
1071 * @param string $cursorName
1072 * @param \FluentCommunity\Framework\Pagination\Cursor|string|null $cursor
1073 * @return \FluentCommunity\Framework\Pagination\CursorPaginator
1074 */
1075 public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
1076 {
1077 $perPage = $perPage ?: $this->model->getPerPage();
1078
1079 return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor);
1080 }
1081
1082 /**
1083 * Ensure the proper order by required for cursor pagination.
1084 *
1085 * @param bool $shouldReverse
1086 * @return \FluentCommunity\Framework\Support\Collection
1087 */
1088 protected function ensureOrderForCursorPagination($shouldReverse = false)
1089 {
1090 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
1091 $this->enforceOrderBy();
1092 }
1093
1094 $reverseDirection = function ($order) {
1095 if (! isset($order['direction'])) {
1096 return $order;
1097 }
1098
1099 $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';
1100
1101 return $order;
1102 };
1103
1104 if ($shouldReverse) {
1105 $this->query->orders = Collection::make($this->query->orders)->map($reverseDirection)->toArray();
1106 $this->query->unionOrders = Collection::make($this->query->unionOrders)->map($reverseDirection)->toArray();
1107 }
1108
1109 $orders = ! empty($this->query->unionOrders) ? $this->query->unionOrders : $this->query->orders;
1110
1111 return Collection::make($orders)
1112 ->filter(fn ($order) => Arr::has($order, 'direction'))
1113 ->values();
1114 }
1115
1116 /**
1117 * Save a new model and return the instance.
1118 *
1119 * @param array $attributes
1120 * @return \FluentCommunity\Framework\Database\Orm\Model|$this
1121 */
1122 public function create(array $attributes = [])
1123 {
1124 $instance = $this->newModelInstance($attributes);
1125
1126 return Helper::tap($instance, function ($instance) {
1127 $instance->save();
1128 });
1129 }
1130
1131 /**
1132 * Save a new model and return the instance. Allow mass-assignment.
1133 *
1134 * @param array $attributes
1135 * @return \FluentCommunity\Framework\Database\Orm\Model|$this
1136 */
1137 public function forceCreate(array $attributes)
1138 {
1139 return $this->model->unguarded(function () use ($attributes) {
1140 return $this->newModelInstance()->create($attributes);
1141 });
1142 }
1143
1144 /**
1145 * Save a new model instance with mass assignment without raising model events.
1146 *
1147 * @param array $attributes
1148 * @return TModel
1149 */
1150 public function forceCreateQuietly(array $attributes = [])
1151 {
1152 return Model::withoutEvents(fn () => $this->forceCreate($attributes));
1153 }
1154
1155 /**
1156 * Update records in the database.
1157 *
1158 * @param array $values
1159 * @return int
1160 */
1161 public function update(array $values)
1162 {
1163 return $this->toBase()->update($this->addUpdatedAtColumn($values));
1164 }
1165
1166 /**
1167 * Insert new records or update the existing ones.
1168 *
1169 * @param array $values
1170 * @param array|string $uniqueBy
1171 * @param array|null $update
1172 * @return int
1173 */
1174 public function upsert(array $values, $uniqueBy, $update = null)
1175 {
1176 if (empty($values)) {
1177 return 0;
1178 }
1179
1180 if (! is_array(reset($values))) {
1181 $values = [$values];
1182 }
1183
1184 if (is_null($update)) {
1185 $update = array_keys(reset($values));
1186 }
1187
1188 return $this->toBase()->upsert(
1189 $this->addTimestampsToUpsertValues($this->addUniqueIdsToUpsertValues($values)),
1190 $uniqueBy,
1191 $this->addUpdatedAtToUpsertColumns($update)
1192 );
1193 }
1194
1195 /**
1196 * Update the column's update timestamp.
1197 *
1198 * @param string|null $column
1199 * @return int|false
1200 */
1201 public function touch($column = null)
1202 {
1203 $time = $this->model->freshTimestamp();
1204
1205 if ($column) {
1206 return $this->toBase()->update([$column => $time]);
1207 }
1208
1209 $column = $this->model->getUpdatedAtColumn();
1210
1211 if (! $this->model->usesTimestamps() || is_null($column)) {
1212 return false;
1213 }
1214
1215 return $this->toBase()->update([$column => $time]);
1216 }
1217
1218 /**
1219 * Increment a column's value by a given amount.
1220 *
1221 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
1222 * @param float|int $amount
1223 * @param array $extra
1224 * @return int
1225 */
1226 public function increment($column, $amount = 1, array $extra = [])
1227 {
1228 return $this->toBase()->increment(
1229 $column, $amount, $this->addUpdatedAtColumn($extra)
1230 );
1231 }
1232
1233 /**
1234 * Decrement a column's value by a given amount.
1235 *
1236 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
1237 * @param float|int $amount
1238 * @param array $extra
1239 * @return int
1240 */
1241 public function decrement($column, $amount = 1, array $extra = [])
1242 {
1243 return $this->toBase()->decrement(
1244 $column, $amount, $this->addUpdatedAtColumn($extra)
1245 );
1246 }
1247
1248 /**
1249 * Add the "updated at" column to an array of values.
1250 *
1251 * @param array $values
1252 * @return array
1253 */
1254 protected function addUpdatedAtColumn(array $values)
1255 {
1256 if (! $this->model->usesTimestamps() ||
1257 is_null($this->model->getUpdatedAtColumn())) {
1258 return $values;
1259 }
1260
1261 $column = $this->model->getUpdatedAtColumn();
1262
1263 $values = array_merge(
1264 [$column => $this->model->freshTimestampString()],
1265 $values
1266 );
1267
1268 $segments = preg_split('/\s+as\s+/i', $this->query->from);
1269
1270 $tableAlias = end($segments);
1271
1272 $connectionName = $this->model->getConnection()->getName();
1273
1274 if ($connectionName === 'sqlite') {
1275 if (count($segments) > 1) {
1276 $qualifiedColumn = $tableAlias . '.' . $column;
1277 } else {
1278 $qualifiedColumn = $column;
1279 }
1280 } else {
1281 $qualifiedColumn = $tableAlias . '.' . $column;
1282 }
1283
1284 $values[$qualifiedColumn] = Arr::get(
1285 $values, $qualifiedColumn, $values[$column]
1286 );
1287
1288 unset($values[$column]);
1289
1290 return $values;
1291 }
1292
1293 /**
1294 * Add unique IDs to the inserted values.
1295 *
1296 * @param array $values
1297 * @return array
1298 */
1299 protected function addUniqueIdsToUpsertValues(array $values)
1300 {
1301 if (! $this->model->usesUniqueIds()) {
1302 return $values;
1303 }
1304
1305 foreach ($this->model->uniqueIds() as $uniqueIdAttribute) {
1306 foreach ($values as &$row) {
1307 if (! array_key_exists($uniqueIdAttribute, $row)) {
1308 $row = array_merge([$uniqueIdAttribute => $this->model->newUniqueId()], $row);
1309 }
1310 }
1311 }
1312
1313 return $values;
1314 }
1315
1316 /**
1317 * Add timestamps to the inserted values.
1318 *
1319 * @param array $values
1320 * @return array
1321 */
1322 protected function addTimestampsToUpsertValues(array $values)
1323 {
1324 if (! $this->model->usesTimestamps()) {
1325 return $values;
1326 }
1327
1328 $timestamp = $this->model->freshTimestampString();
1329
1330 $columns = array_filter([
1331 $this->model->getCreatedAtColumn(),
1332 $this->model->getUpdatedAtColumn(),
1333 ]);
1334
1335 foreach ($columns as $column) {
1336 foreach ($values as &$row) {
1337 $row = array_merge([$column => $timestamp], $row);
1338 }
1339 }
1340
1341 return $values;
1342 }
1343
1344 /**
1345 * Add the "updated at" column to the updated columns.
1346 *
1347 * @param array $update
1348 * @return array
1349 */
1350 protected function addUpdatedAtToUpsertColumns(array $update)
1351 {
1352 if (! $this->model->usesTimestamps()) {
1353 return $update;
1354 }
1355
1356 $column = $this->model->getUpdatedAtColumn();
1357
1358 if (! is_null($column) &&
1359 ! array_key_exists($column, $update) &&
1360 ! in_array($column, $update)) {
1361 $update[] = $column;
1362 }
1363
1364 return $update;
1365 }
1366
1367 /**
1368 * Delete records from the database.
1369 *
1370 * @return mixed
1371 */
1372 public function delete()
1373 {
1374 if (isset($this->onDelete)) {
1375 return call_user_func($this->onDelete, $this);
1376 }
1377
1378 return $this->toBase()->delete();
1379 }
1380
1381 /**
1382 * Run the default delete function on the builder.
1383 *
1384 * Since we do not apply scopes here, the row will actually be deleted.
1385 *
1386 * @return mixed
1387 */
1388 public function forceDelete()
1389 {
1390 return $this->query->delete();
1391 }
1392
1393 /**
1394 * Register a replacement for the default delete function.
1395 *
1396 * @param \Closure $callback
1397 * @return void
1398 */
1399 public function onDelete(Closure $callback)
1400 {
1401 $this->onDelete = $callback;
1402 }
1403
1404 /**
1405 * Determine if the given model has a scope.
1406 *
1407 * @param string $scope
1408 * @return bool
1409 */
1410 public function hasNamedScope($scope)
1411 {
1412 return $this->model && $this->model->hasNamedScope($scope);
1413 }
1414
1415 /**
1416 * Call the given local model scopes.
1417 *
1418 * @param array|string $scopes
1419 * @return static|mixed
1420 */
1421 public function scopes($scopes)
1422 {
1423 $builder = $this;
1424
1425 foreach (Arr::wrap($scopes) as $scope => $parameters) {
1426 // If the scope key is an integer, then the scope was passed as the value and
1427 // the parameter list is empty, so we will format the scope name and these
1428 // parameters here. Then, we'll be ready to call the scope on the model.
1429 if (is_int($scope)) {
1430 [$scope, $parameters] = [$parameters, []];
1431 }
1432
1433 // Next we'll pass the scope callback to the callScope method which will take
1434 // care of grouping the "wheres" properly so the logical order doesn't get
1435 // messed up when adding scopes. Then we'll return back out the builder.
1436 $builder = $builder->callNamedScope(
1437 $scope, Arr::wrap($parameters)
1438 );
1439 }
1440
1441 return $builder;
1442 }
1443
1444 /**
1445 * Apply the scopes to the Orm builder instance and return it.
1446 *
1447 * @return static
1448 */
1449 public function applyScopes()
1450 {
1451 if (! $this->scopes) {
1452 return $this;
1453 }
1454
1455 $builder = clone $this;
1456
1457 foreach ($this->scopes as $identifier => $scope) {
1458 if (! isset($builder->scopes[$identifier])) {
1459 continue;
1460 }
1461
1462 $builder->callScope(function (self $builder) use ($scope) {
1463 // If the scope is a Closure we will just go ahead and call the scope with the
1464 // builder instance. The "callScope" method will properly group the clauses
1465 // that are added to this query so "where" clauses maintain proper logic.
1466 if ($scope instanceof Closure) {
1467 $scope($builder);
1468 }
1469
1470 // If the scope is a scope object, we will call the apply method on this scope
1471 // passing in the builder and the model instance. After we run all of these
1472 // scopes we will return back the builder instance to the outside caller.
1473 if ($scope instanceof Scope) {
1474 $scope->apply($builder, $this->getModel());
1475 }
1476 });
1477 }
1478
1479 return $builder;
1480 }
1481
1482 /**
1483 * Apply the given scope on the current builder instance.
1484 *
1485 * @param callable $scope
1486 * @param array $parameters
1487 * @return mixed
1488 */
1489 protected function callScope(callable $scope, array $parameters = [])
1490 {
1491 array_unshift($parameters, $this);
1492
1493 $query = $this->getQuery();
1494
1495 // We will keep track of how many wheres are on the query before running the
1496 // scope so that we can properly group the added scope constraints in the
1497 // query as their own isolated nested where statement and avoid issues.
1498 $originalWhereCount = is_null($query->wheres)
1499 ? 0 : count($query->wheres);
1500
1501 $result = $scope(...$parameters) ?? $this;
1502
1503 if (count((array) $query->wheres) > $originalWhereCount) {
1504 $this->addNewWheresWithinGroup($query, $originalWhereCount);
1505 }
1506
1507 return $result;
1508 }
1509
1510 /**
1511 * Apply the given named scope on the current builder instance.
1512 *
1513 * @param string $scope
1514 * @param array $parameters
1515 * @return mixed
1516 */
1517 protected function callNamedScope($scope, array $parameters = [])
1518 {
1519 return $this->callScope(function (...$parameters) use ($scope) {
1520 return $this->model->callNamedScope($scope, $parameters);
1521 }, $parameters);
1522 }
1523
1524 /**
1525 * Nest where conditions by slicing them at the given where count.
1526 *
1527 * @param \FluentCommunity\Framework\Database\Query\Builder $query
1528 * @param int $originalWhereCount
1529 * @return void
1530 */
1531 protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
1532 {
1533 // Here, we totally remove all of the where clauses since we are going to
1534 // rebuild them as nested queries by slicing the groups of wheres into
1535 // their own sections. This is to prevent any confusing logic order.
1536 $allWheres = $query->wheres;
1537
1538 $query->wheres = [];
1539
1540 $this->groupWhereSliceForScope(
1541 $query, array_slice($allWheres, 0, $originalWhereCount)
1542 );
1543
1544 $this->groupWhereSliceForScope(
1545 $query, array_slice($allWheres, $originalWhereCount)
1546 );
1547 }
1548
1549 /**
1550 * Slice where conditions at the given offset and add them to the query as a nested condition.
1551 *
1552 * @param \FluentCommunity\Framework\Database\Query\Builder $query
1553 * @param array $whereSlice
1554 * @return void
1555 */
1556 protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
1557 {
1558 $whereBooleans = Collection::make($whereSlice)->pluck('boolean');
1559
1560 // Here we'll check if the given subset of where clauses contains any "or"
1561 // booleans and in this case create a nested where expression. That way
1562 // we don't add any unnecessary nesting thus keeping the query clean.
1563 if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) {
1564 $query->wheres[] = $this->createNestedWhere(
1565 $whereSlice, str_replace(' not', '', $whereBooleans->first())
1566 );
1567 } else {
1568 $query->wheres = array_merge($query->wheres, $whereSlice);
1569 }
1570 }
1571
1572 /**
1573 * Create a where array with nested where conditions.
1574 *
1575 * @param array $whereSlice
1576 * @param string $boolean
1577 * @return array
1578 */
1579 protected function createNestedWhere($whereSlice, $boolean = 'and')
1580 {
1581 $whereGroup = $this->getQuery()->forNestedWhere();
1582
1583 $whereGroup->wheres = $whereSlice;
1584
1585 return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
1586 }
1587
1588 /**
1589 * Set the relationships that should be eager loaded.
1590 *
1591 * @param string|array $relations
1592 * @param string|\Closure|null $callback
1593 * @return $this
1594 */
1595 public function with($relations, $callback = null)
1596 {
1597 if ($callback instanceof Closure) {
1598 $eagerLoad = $this->parseWithRelations([$relations => $callback]);
1599 } else {
1600 $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
1601 }
1602
1603 $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
1604
1605 return $this;
1606 }
1607
1608 /**
1609 * Prevent the specified relations from being eager loaded.
1610 *
1611 * @param mixed $relations
1612 * @return $this
1613 */
1614 public function without($relations)
1615 {
1616 $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip(
1617 is_string($relations) ? func_get_args() : $relations
1618 ));
1619
1620 return $this;
1621 }
1622
1623 /**
1624 * Set the relationships that should be eager loaded while removing any previously added eager loading specifications.
1625 *
1626 * @param mixed $relations
1627 * @return $this
1628 */
1629 public function withOnly($relations)
1630 {
1631 $this->eagerLoad = [];
1632
1633 return $this->with($relations);
1634 }
1635
1636 /**
1637 * Create a new instance of the model being queried.
1638 *
1639 * @param array $attributes
1640 * @return \FluentCommunity\Framework\Database\Orm\Model|static
1641 */
1642 public function newModelInstance($attributes = [])
1643 {
1644 return $this->model->newInstance($attributes)->setConnection(
1645 $this->query->getConnection()->getName()
1646 );
1647 }
1648
1649 /**
1650 * Parse a list of relations into individuals.
1651 *
1652 * @param array $relations
1653 * @return array
1654 */
1655 protected function parseWithRelations(array $relations)
1656 {
1657 if ($relations === []) {
1658 return [];
1659 }
1660
1661 $results = [];
1662
1663 foreach ($this->prepareNestedWithRelationships($relations) as $name => $constraints) {
1664 // We need to separate out any nested includes, which allows the developers
1665 // to load deep relationships using "dots" without stating each level of
1666 // the relationship with its own key in the array of eager-load names.
1667 $results = $this->addNestedWiths($name, $results);
1668
1669 $results[$name] = $constraints;
1670 }
1671
1672 return $results;
1673 }
1674
1675 protected function prepareNestedWithRelationships($relations, $prefix = '')
1676 {
1677 $preparedRelationships = [];
1678
1679 if ($prefix !== '') {
1680 $prefix .= '.';
1681 }
1682
1683 // If any of the relationships are formatted with the [$attribute => array()]
1684 // syntax, we shall loop over the nested relations and prepend each key of
1685 // this array while flattening into the traditional dot notation format.
1686 foreach ($relations as $key => $value) {
1687 if (! is_string($key) || ! is_array($value)) {
1688 continue;
1689 }
1690
1691 [$attribute, $attributeSelectConstraint] = $this->parseNameAndAttributeSelectionConstraint($key);
1692
1693 $preparedRelationships = array_merge(
1694 $preparedRelationships,
1695 ["{$prefix}{$attribute}" => $attributeSelectConstraint],
1696 $this->prepareNestedWithRelationships($value, "{$prefix}{$attribute}"),
1697 );
1698
1699 unset($relations[$key]);
1700 }
1701
1702 // We now know that the remaining relationships are in a dot notation format
1703 // and may be a string or Closure. We'll loop over them and ensure all of
1704 // the present Closures are merged + strings are made into constraints.
1705 foreach ($relations as $key => $value) {
1706 if (is_numeric($key) && is_string($value)) {
1707 [$key, $value] = $this->parseNameAndAttributeSelectionConstraint($value);
1708 }
1709
1710 $preparedRelationships[$prefix.$key] = $this->combineConstraints([
1711 $value,
1712 $preparedRelationships[$prefix.$key] ?? static function () {
1713 //
1714 },
1715 ]);
1716 }
1717
1718 return $preparedRelationships;
1719 }
1720
1721 /**
1722 * Combine an array of constraints into a single constraint.
1723 *
1724 * @param array $constraints
1725 * @return \Closure
1726 */
1727 protected function combineConstraints(array $constraints)
1728 {
1729 return function ($builder) use ($constraints) {
1730 foreach ($constraints as $constraint) {
1731 $builder = $constraint($builder) ?? $builder;
1732 }
1733
1734 return $builder;
1735 };
1736 }
1737
1738 /**
1739 * Parse the attribute select constraints from the name.
1740 *
1741 * @param string $name
1742 * @return array
1743 */
1744 protected function parseNameAndAttributeSelectionConstraint($name)
1745 {
1746 return str_contains($name, ':')
1747 ? $this->createSelectWithConstraint($name)
1748 : [$name, static function () {
1749 //
1750 }];
1751 }
1752
1753 /**
1754 * Create a constraint to select the given columns for the relation.
1755 *
1756 * @param string $name
1757 * @return array
1758 */
1759 protected function createSelectWithConstraint($name)
1760 {
1761 return [explode(':', $name)[0], static function ($query) use ($name) {
1762 $query->select(array_map(static function ($column) use ($query) {
1763 if (Str::contains($column, '.')) {
1764 return $column;
1765 }
1766
1767 return $query instanceof BelongsToMany
1768 ? $query->getRelated()->getTable().'.'.$column
1769 : $column;
1770 }, explode(',', explode(':', $name)[1])));
1771 }];
1772 }
1773
1774 /**
1775 * Parse the nested relationships in a relation.
1776 *
1777 * @param string $name
1778 * @param array $results
1779 * @return array
1780 */
1781 protected function addNestedWiths($name, $results)
1782 {
1783 $progress = [];
1784
1785 // If the relation has already been set on the result array, we will not set it
1786 // again, since that would override any constraints that were already placed
1787 // on the relationships. We will only set the ones that are not specified.
1788 foreach (explode('.', $name) as $segment) {
1789 $progress[] = $segment;
1790
1791 if (! isset($results[$last = implode('.', $progress)])) {
1792 $results[$last] = static function () {
1793 //
1794 };
1795 }
1796 }
1797
1798 return $results;
1799 }
1800
1801 /**
1802 * Apply query-time casts to the model instance.
1803 *
1804 * @param array $casts
1805 * @return $this
1806 */
1807 public function withCasts($casts)
1808 {
1809 $this->model->mergeCasts($casts);
1810
1811 return $this;
1812 }
1813
1814 /**
1815 * Execute the given Closure within a transaction savepoint if needed.
1816 *
1817 * @template TModelValue
1818 *
1819 * @param \Closure(): TModelValue $scope
1820 * @return TModelValue
1821 */
1822 public function withSavepointIfNeeded(Closure $scope)
1823 {
1824 return $this->getQuery()->getConnection()->transactionLevel() > 0
1825 ? $this->getQuery()->getConnection()->transaction($scope)
1826 : $scope();
1827 }
1828
1829 /**
1830 * Get the Eloquent builder instances that are used in the union of the query.
1831 *
1832 * @return \FluentCommunity\Framework\Support\Collection
1833 */
1834 protected function getUnionBuilders()
1835 {
1836 return isset($this->query->unions)
1837 ? Helper::collect($this->query->unions)->pluck('query')
1838 : Helper::collect();
1839 }
1840
1841 /**
1842 * Get the underlying query builder instance.
1843 *
1844 * @return \FluentCommunity\Framework\Database\Query\Builder
1845 */
1846 public function getQuery()
1847 {
1848 return $this->query;
1849 }
1850
1851 /**
1852 * Set the underlying query builder instance.
1853 *
1854 * @param \FluentCommunity\Framework\Database\Query\Builder $query
1855 * @return $this
1856 */
1857 public function setQuery($query)
1858 {
1859 $this->query = $query;
1860
1861 return $this;
1862 }
1863
1864 /**
1865 * Get a base query builder instance.
1866 *
1867 * @return \FluentCommunity\Framework\Database\Query\Builder
1868 */
1869 public function toBase()
1870 {
1871 return $this->applyScopes()->getQuery();
1872 }
1873
1874 /**
1875 * Get the relationships being eagerly loaded.
1876 *
1877 * @return array
1878 */
1879 public function getEagerLoads()
1880 {
1881 return $this->eagerLoad;
1882 }
1883
1884 /**
1885 * Set the relationships being eagerly loaded.
1886 *
1887 * @param array $eagerLoad
1888 * @return $this
1889 */
1890 public function setEagerLoads(array $eagerLoad)
1891 {
1892 $this->eagerLoad = $eagerLoad;
1893
1894 return $this;
1895 }
1896
1897 /**
1898 * Indicate that the given relationships should not be eagerly loaded.
1899 *
1900 * @param array $relations
1901 * @return $this
1902 */
1903 public function withoutEagerLoad(array $relations)
1904 {
1905 $relations = array_diff(array_keys($this->model->getRelations()), $relations);
1906
1907 return $this->with($relations);
1908 }
1909
1910 /**
1911 * Flush the relationships being eagerly loaded.
1912 *
1913 * @return $this
1914 */
1915 public function withoutEagerLoads()
1916 {
1917 return $this->setEagerLoads([]);
1918 }
1919
1920 /**
1921 * Get the default key name of the table.
1922 *
1923 * @return string
1924 */
1925 protected function defaultKeyName()
1926 {
1927 return $this->getModel()->getKeyName();
1928 }
1929
1930 /**
1931 * Get the model instance being queried.
1932 *
1933 * @return \FluentCommunity\Framework\Database\Orm\Model|static
1934 */
1935 public function getModel()
1936 {
1937 return $this->model;
1938 }
1939
1940 /**
1941 * Set a model instance for the model being queried.
1942 *
1943 * @param \FluentCommunity\Framework\Database\Orm\Model $model
1944 * @return $this
1945 */
1946 public function setModel(Model $model)
1947 {
1948 $this->model = $model;
1949
1950 $this->query->from($model->getTable());
1951
1952 return $this;
1953 }
1954
1955 /**
1956 * Qualify the given column name by the model's table.
1957 *
1958 * @param string|\FluentCommunity\Framework\Database\Query\Expression $column
1959 * @return string
1960 */
1961 public function qualifyColumn($column)
1962 {
1963 $column = $column instanceof Expression ? $column->getValue(
1964 $this->getGrammar()
1965 ) : $column;
1966
1967 return $this->model->qualifyColumn($column);
1968 }
1969
1970 /**
1971 * Qualify the given columns with the model's table.
1972 *
1973 * @param array|\FluentCommunity\Framework\Database\Query\Expression $columns
1974 * @return array
1975 */
1976 public function qualifyColumns($columns)
1977 {
1978 return $this->model->qualifyColumns($columns);
1979 }
1980
1981 /**
1982 * Get the given macro by name.
1983 *
1984 * @param string $name
1985 * @return \Closure
1986 */
1987 public function getMacro($name)
1988 {
1989 return Arr::get($this->localMacros, $name);
1990 }
1991
1992 /**
1993 * Checks if a macro is registered.
1994 *
1995 * @param string $name
1996 * @return bool
1997 */
1998 public function hasMacro($name)
1999 {
2000 return isset($this->localMacros[$name]);
2001 }
2002
2003 /**
2004 * Get the given global macro by name.
2005 *
2006 * @param string $name
2007 * @return \Closure
2008 */
2009 public static function getGlobalMacro($name)
2010 {
2011 return Arr::get(static::$macros, $name);
2012 }
2013
2014 /**
2015 * Checks if a global macro is registered.
2016 *
2017 * @param string $name
2018 * @return bool
2019 */
2020 public static function hasGlobalMacro($name)
2021 {
2022 return isset(static::$macros[$name]);
2023 }
2024
2025 /**
2026 * Dynamically access builder proxies.
2027 *
2028 * @param string $key
2029 * @return mixed
2030 *
2031 * @throws \Exception
2032 */
2033 public function __get($key)
2034 {
2035 if (in_array($key, ['orWhere', 'whereNot', 'orWhereNot'])) {
2036 return new HigherOrderBuilderProxy($this, $key);
2037 }
2038
2039 if (in_array($key, $this->propertyPassthru)) {
2040 return $this->toBase()->{$key};
2041 }
2042
2043 throw new Exception("Property [{$key}] does not exist on the Eloquent builder instance.");
2044 }
2045
2046 /**
2047 * Dynamically handle calls into the query instance.
2048 *
2049 * @param string $method
2050 * @param array $parameters
2051 * @return mixed
2052 */
2053 public function __call($method, $parameters)
2054 {
2055 if ($method === 'macro') {
2056 $this->localMacros[$parameters[0]] = $parameters[1];
2057
2058 return;
2059 }
2060
2061 if ($this->hasMacro($method)) {
2062 array_unshift($parameters, $this);
2063
2064 return $this->localMacros[$method](...$parameters);
2065 }
2066
2067 if (static::hasGlobalMacro($method)) {
2068 $callable = static::$macros[$method];
2069
2070 if ($callable instanceof Closure) {
2071 $callable = $callable->bindTo($this, static::class);
2072 }
2073
2074 return $callable(...$parameters);
2075 }
2076
2077 if ($this->hasNamedScope($method)) {
2078 return $this->callNamedScope($method, $parameters);
2079 }
2080
2081 if (in_array(strtolower($method), $this->passthru)) {
2082 return $this->toBase()->{$method}(...$parameters);
2083 }
2084
2085 $this->forwardCallTo($this->query, $method, $parameters);
2086
2087 return $this;
2088 }
2089
2090 /**
2091 * Dynamically handle calls into the query instance.
2092 *
2093 * @param string $method
2094 * @param array $parameters
2095 * @return mixed
2096 *
2097 * @throws \BadMethodCallException
2098 */
2099 public static function __callStatic($method, $parameters)
2100 {
2101 if ($method === 'macro') {
2102 static::$macros[$parameters[0]] = $parameters[1];
2103
2104 return;
2105 }
2106
2107 if ($method === 'mixin') {
2108 return static::registerMixin($parameters[0], $parameters[1] ?? true);
2109 }
2110
2111 if (! static::hasGlobalMacro($method)) {
2112 static::throwBadMethodCallException($method);
2113 }
2114
2115 $callable = static::$macros[$method];
2116
2117 if ($callable instanceof Closure) {
2118 $callable = $callable->bindTo(null, static::class);
2119 }
2120
2121 return $callable(...$parameters);
2122 }
2123
2124 /**
2125 * Register the given mixin with the builder.
2126 *
2127 * @param string $mixin
2128 * @param bool $replace
2129 * @return void
2130 */
2131 protected static function registerMixin($mixin, $replace)
2132 {
2133 $methods = (new ReflectionClass($mixin))->getMethods(
2134 ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
2135 );
2136
2137 foreach ($methods as $method) {
2138 if ($replace || ! static::hasGlobalMacro($method->name)) {
2139 static::macro($method->name, $method->invoke($mixin));
2140 }
2141 }
2142 }
2143
2144 /**
2145 * Clone the Orm query builder.
2146 *
2147 * @return static
2148 */
2149 public function clone()
2150 {
2151 return clone $this;
2152 }
2153
2154 /**
2155 * Force a clone of the underlying query builder when cloning.
2156 *
2157 * @return void
2158 */
2159 public function __clone()
2160 {
2161 $this->query = clone $this->query;
2162 }
2163 }
2164