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

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

2,182 lines 60.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Database\Orm;
4
5 use Closure;
6 use Exception;
7 use ReflectionClass;
8 use ReflectionMethod;
9 use BadMethodCallException;
10 use FluentBoards\Framework\Support\Arr;
11 use FluentBoards\Framework\Support\Str;
12 use FluentBoards\Framework\Support\Helper;
13 use FluentBoards\Framework\Pagination\Paginator;
14 use FluentBoards\Framework\Support\ForwardsCalls;
15 use FluentBoards\Framework\Support\ArrayableInterface;
16 use FluentBoards\Framework\Database\Query\Expression;
17 use FluentBoards\Framework\Database\Concerns\BuildsQueries;
18 use FluentBoards\Framework\Database\RecordsNotFoundException;
19 use FluentBoards\Framework\Database\Orm\Relations\Relation;
20 use FluentBoards\Framework\Database\Orm\Relations\BelongsToMany;
21 use FluentBoards\Framework\Database\Orm\RelationNotFoundException;
22 use FluentBoards\Framework\Database\Orm\Concerns\QueriesRelationships;
23 use FluentBoards\Framework\Database\UniqueConstraintViolationException;
24 use FluentBoards\Framework\Database\Query\Builder as QueryBuilder;
25
26 /**
27 * @property-read HigherOrderBuilderProxy $orWhere
28 *
29 * @mixin \FluentBoards\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 \FluentBoards\Framework\Database\Query\Builder
41 */
42 protected $query;
43
44 /**
45 * The model being queried.
46 *
47 * @var \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\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|\FluentBoards\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|\FluentBoards\Framework\Database\Query\Expression $column
356 * @param mixed $operator
357 * @param mixed $value
358 * @param string $boolean
359 * @return \FluentBoards\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|\FluentBoards\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|\FluentBoards\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|\FluentBoards\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|\FluentBoards\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|\FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\Framework\Database\Orm\Model|\FluentBoards\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 \FluentBoards\Framework\Support\ArrayableInterface|array $ids
499 * @param array $columns
500 * @return \FluentBoards\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 \FluentBoards\Framework\Database\Orm\Model|\FluentBoards\Framework\Database\Orm\Collection|static|static[]
519 *
520 * @throws \FluentBoards\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 \FluentBoards\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 (\FluentBoards\Framework\Support\ArrayableInterface<array-key, mixed>|array<mixed>)
573 * ? \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\Framework\Database\Orm\Model|static
663 *
664 * @throws \FluentBoards\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 \FluentBoards\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 \FluentBoards\Framework\Database\Orm\Model
702 *
703 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException
704 * @throws \FluentBoards\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|\FluentBoards\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|\FluentBoards\Framework\Database\Query\Expression $column
736 * @return mixed
737 *
738 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException<TModel>
739 * @throws \FluentBoards\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|\FluentBoards\Framework\Database\Query\Expression $column
754 * @return mixed
755 *
756 * @throws \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 \FluentBoards\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(
959 $this->newModelInstance()->newCollection([$model])
960 )->first();
961 })->reject(fn ($model) => is_null($model));
962 }
963
964 /**
965 * Get a lazy collection for the given query using raw query.
966 *
967 * @return \FluentBoards\Framework\Support\LazyCollection
968 */
969 public function rawCursor()
970 {
971 return $this->applyScopes()->query->rawCursor()->map(function ($record) {
972 $model = $this->newModelInstance()->newFromBuilder($record);
973
974 return $this->applyAfterQueryCallbacks(
975 $this->newModelInstance()->newCollection([$model])
976 )->first();
977 })->reject(fn ($model) => is_null($model));
978 }
979
980 /**
981 * Add a generic "order by" clause if the query doesn't already have one.
982 *
983 * @return void
984 */
985 protected function enforceOrderBy()
986 {
987 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
988 $this->orderBy($this->model->getQualifiedKeyName(), 'asc');
989 }
990 }
991
992 /**
993 * Get an array with the values of a given column.
994 *
995 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
996 * @param string|null $key
997 * @return \FluentBoards\Framework\Support\Collection
998 */
999 public function pluck($column, $key = null)
1000 {
1001 $results = $this->toBase()->pluck($column, $key);
1002
1003 $column = $column instanceof Expression ? $column->getValue(
1004 $this->getGrammar()
1005 ) : $column;
1006
1007 $column = Str::after($column, "{$this->model->getTable()}.");
1008
1009 // If the model has a mutator for the requested column, we will spin through
1010 // the results and mutate the values so that the mutated version of these
1011 // columns are returned as you would expect from these Eloquent models.
1012 if (! $this->model->hasGetMutator($column) &&
1013 ! $this->model->hasCast($column) &&
1014 ! in_array($column, $this->model->getDates())) {
1015 return $results;
1016 }
1017
1018 return $this->applyAfterQueryCallbacks(
1019 $results->map(function ($value) use ($column) {
1020 return $this->model->newFromBuilder([$column => $value])->{$column};
1021 })
1022 );
1023 }
1024
1025 /**
1026 * Paginate the given query.
1027 *
1028 * @param int|null|\Closure $perPage
1029 * @param array|string $columns
1030 * @param string $pageName
1031 * @param int|null $page
1032 * @param \Closure|int|null $total
1033 * @return \FluentBoards\Framework\Pagination\LengthAwarePaginator
1034 *
1035 * @throws \InvalidArgumentException
1036 */
1037 public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)
1038 {
1039 $page = $page ?: Paginator::resolveCurrentPage($pageName);
1040
1041 $total = Helper::value($total) ?? $this->toBase()->getCountForPagination();
1042
1043 $perPage = ($perPage instanceof Closure
1044 ? $perPage($total)
1045 : $perPage
1046 ) ?: $this->model->getPerPage();
1047
1048 $results = $total
1049 ? $this->forPage($page, $perPage)->get($columns)
1050 : $this->model->newCollection();
1051
1052 return $this->paginator($results, $total, $perPage, $page, [
1053 'path' => Paginator::resolveCurrentPath(),
1054 'pageName' => $pageName,
1055 ]);
1056 }
1057
1058 /**
1059 * Paginate the given query into a simple paginator.
1060 *
1061 * @param int|null $perPage
1062 * @param array $columns
1063 * @param string $pageName
1064 * @param int|null $page
1065 * @return \FluentBoards\Framework\Pagination\PaginatorInterface
1066 */
1067 public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
1068 {
1069 $page = $page ?: Paginator::resolveCurrentPage($pageName);
1070
1071 $perPage = $perPage ?: $this->model->getPerPage();
1072
1073 // Next we will set the limit and offset for this query so that when we get the
1074 // results we get the proper section of results. Then, we'll create the full
1075 // paginator instances for these results with the given page and per page.
1076 $this->skip(($page - 1) * $perPage)->take($perPage + 1);
1077
1078 return $this->simplePaginator($this->get($columns), $perPage, $page, [
1079 'path' => Paginator::resolveCurrentPath(),
1080 'pageName' => $pageName,
1081 ]);
1082 }
1083
1084 /**
1085 * Paginate the given query into a cursor paginator.
1086 *
1087 * @param int|null $perPage
1088 * @param array $columns
1089 * @param string $cursorName
1090 * @param \FluentBoards\Framework\Pagination\Cursor|string|null $cursor
1091 * @return \FluentBoards\Framework\Pagination\CursorPaginator
1092 */
1093 public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
1094 {
1095 $perPage = $perPage ?: $this->model->getPerPage();
1096
1097 return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor);
1098 }
1099
1100 /**
1101 * Ensure the proper order by required for cursor pagination.
1102 *
1103 * @param bool $shouldReverse
1104 * @return \FluentBoards\Framework\Support\Collection
1105 */
1106 protected function ensureOrderForCursorPagination($shouldReverse = false)
1107 {
1108 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
1109 $this->enforceOrderBy();
1110 }
1111
1112 $reverseDirection = function ($order) {
1113 if (! isset($order['direction'])) {
1114 return $order;
1115 }
1116
1117 $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';
1118
1119 return $order;
1120 };
1121
1122 if ($shouldReverse) {
1123 $this->query->orders = Collection::make($this->query->orders)->map($reverseDirection)->toArray();
1124 $this->query->unionOrders = Collection::make($this->query->unionOrders)->map($reverseDirection)->toArray();
1125 }
1126
1127 $orders = ! empty($this->query->unionOrders) ? $this->query->unionOrders : $this->query->orders;
1128
1129 return Collection::make($orders)
1130 ->filter(fn ($order) => Arr::has($order, 'direction'))
1131 ->values();
1132 }
1133
1134 /**
1135 * Save a new model and return the instance.
1136 *
1137 * @param array $attributes
1138 * @return \FluentBoards\Framework\Database\Orm\Model|$this
1139 */
1140 public function create(array $attributes = [])
1141 {
1142 $instance = $this->newModelInstance($attributes);
1143
1144 return Helper::tap($instance, function ($instance) {
1145 $instance->save();
1146 });
1147 }
1148
1149 /**
1150 * Save a new model and return the instance. Allow mass-assignment.
1151 *
1152 * @param array $attributes
1153 * @return \FluentBoards\Framework\Database\Orm\Model|$this
1154 */
1155 public function forceCreate(array $attributes)
1156 {
1157 return $this->model->unguarded(function () use ($attributes) {
1158 return $this->newModelInstance()->create($attributes);
1159 });
1160 }
1161
1162 /**
1163 * Save a new model instance with mass assignment without raising model events.
1164 *
1165 * @param array $attributes
1166 * @return TModel
1167 */
1168 public function forceCreateQuietly(array $attributes = [])
1169 {
1170 return Model::withoutEvents(fn () => $this->forceCreate($attributes));
1171 }
1172
1173 /**
1174 * Update records in the database.
1175 *
1176 * @param array $values
1177 * @return int
1178 */
1179 public function update(array $values)
1180 {
1181 return $this->toBase()->update($this->addUpdatedAtColumn($values));
1182 }
1183
1184 /**
1185 * Insert new records or update the existing ones.
1186 *
1187 * @param array $values
1188 * @param array|string $uniqueBy
1189 * @param array|null $update
1190 * @return int
1191 */
1192 public function upsert(array $values, $uniqueBy, $update = null)
1193 {
1194 if (empty($values)) {
1195 return 0;
1196 }
1197
1198 if (! is_array(reset($values))) {
1199 $values = [$values];
1200 }
1201
1202 if (is_null($update)) {
1203 $update = array_keys(reset($values));
1204 }
1205
1206 return $this->toBase()->upsert(
1207 $this->addTimestampsToUpsertValues($this->addUniqueIdsToUpsertValues($values)),
1208 $uniqueBy,
1209 $this->addUpdatedAtToUpsertColumns($update)
1210 );
1211 }
1212
1213 /**
1214 * Update the column's update timestamp.
1215 *
1216 * @param string|null $column
1217 * @return int|false
1218 */
1219 public function touch($column = null)
1220 {
1221 $time = $this->model->freshTimestamp();
1222
1223 if ($column) {
1224 return $this->toBase()->update([$column => $time]);
1225 }
1226
1227 $column = $this->model->getUpdatedAtColumn();
1228
1229 if (! $this->model->usesTimestamps() || is_null($column)) {
1230 return false;
1231 }
1232
1233 return $this->toBase()->update([$column => $time]);
1234 }
1235
1236 /**
1237 * Increment a column's value by a given amount.
1238 *
1239 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1240 * @param float|int $amount
1241 * @param array $extra
1242 * @return int
1243 */
1244 public function increment($column, $amount = 1, array $extra = [])
1245 {
1246 return $this->toBase()->increment(
1247 $column, $amount, $this->addUpdatedAtColumn($extra)
1248 );
1249 }
1250
1251 /**
1252 * Decrement a column's value by a given amount.
1253 *
1254 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1255 * @param float|int $amount
1256 * @param array $extra
1257 * @return int
1258 */
1259 public function decrement($column, $amount = 1, array $extra = [])
1260 {
1261 return $this->toBase()->decrement(
1262 $column, $amount, $this->addUpdatedAtColumn($extra)
1263 );
1264 }
1265
1266 /**
1267 * Add the "updated at" column to an array of values.
1268 *
1269 * @param array $values
1270 * @return array
1271 */
1272 protected function addUpdatedAtColumn(array $values)
1273 {
1274 if (! $this->model->usesTimestamps() ||
1275 is_null($this->model->getUpdatedAtColumn())) {
1276 return $values;
1277 }
1278
1279 $column = $this->model->getUpdatedAtColumn();
1280
1281 $values = array_merge(
1282 [$column => $this->model->freshTimestampString()],
1283 $values
1284 );
1285
1286 $segments = preg_split('/\s+as\s+/i', $this->query->from);
1287
1288 $tableAlias = end($segments);
1289
1290 $connectionName = $this->model->getConnection()->getName();
1291
1292 if ($connectionName === 'sqlite') {
1293 if (count($segments) > 1) {
1294 $qualifiedColumn = $tableAlias . '.' . $column;
1295 } else {
1296 $qualifiedColumn = $column;
1297 }
1298 } else {
1299 $qualifiedColumn = $tableAlias . '.' . $column;
1300 }
1301
1302 $values[$qualifiedColumn] = Arr::get(
1303 $values, $qualifiedColumn, $values[$column]
1304 );
1305
1306 unset($values[$column]);
1307
1308 return $values;
1309 }
1310
1311 /**
1312 * Add unique IDs to the inserted values.
1313 *
1314 * @param array $values
1315 * @return array
1316 */
1317 protected function addUniqueIdsToUpsertValues(array $values)
1318 {
1319 if (! $this->model->usesUniqueIds()) {
1320 return $values;
1321 }
1322
1323 foreach ($this->model->uniqueIds() as $uniqueIdAttribute) {
1324 foreach ($values as &$row) {
1325 if (! array_key_exists($uniqueIdAttribute, $row)) {
1326 $row = array_merge([$uniqueIdAttribute => $this->model->newUniqueId()], $row);
1327 }
1328 }
1329 }
1330
1331 return $values;
1332 }
1333
1334 /**
1335 * Add timestamps to the inserted values.
1336 *
1337 * @param array $values
1338 * @return array
1339 */
1340 protected function addTimestampsToUpsertValues(array $values)
1341 {
1342 if (! $this->model->usesTimestamps()) {
1343 return $values;
1344 }
1345
1346 $timestamp = $this->model->freshTimestampString();
1347
1348 $columns = array_filter([
1349 $this->model->getCreatedAtColumn(),
1350 $this->model->getUpdatedAtColumn(),
1351 ]);
1352
1353 foreach ($columns as $column) {
1354 foreach ($values as &$row) {
1355 $row = array_merge([$column => $timestamp], $row);
1356 }
1357 }
1358
1359 return $values;
1360 }
1361
1362 /**
1363 * Add the "updated at" column to the updated columns.
1364 *
1365 * @param array $update
1366 * @return array
1367 */
1368 protected function addUpdatedAtToUpsertColumns(array $update)
1369 {
1370 if (! $this->model->usesTimestamps()) {
1371 return $update;
1372 }
1373
1374 $column = $this->model->getUpdatedAtColumn();
1375
1376 if (! is_null($column) &&
1377 ! array_key_exists($column, $update) &&
1378 ! in_array($column, $update)) {
1379 $update[] = $column;
1380 }
1381
1382 return $update;
1383 }
1384
1385 /**
1386 * Delete records from the database.
1387 *
1388 * @return mixed
1389 */
1390 public function delete()
1391 {
1392 if (isset($this->onDelete)) {
1393 return call_user_func($this->onDelete, $this);
1394 }
1395
1396 return $this->toBase()->delete();
1397 }
1398
1399 /**
1400 * Run the default delete function on the builder.
1401 *
1402 * Since we do not apply scopes here, the row will actually be deleted.
1403 *
1404 * @return mixed
1405 */
1406 public function forceDelete()
1407 {
1408 return $this->query->delete();
1409 }
1410
1411 /**
1412 * Register a replacement for the default delete function.
1413 *
1414 * @param \Closure $callback
1415 * @return void
1416 */
1417 public function onDelete(Closure $callback)
1418 {
1419 $this->onDelete = $callback;
1420 }
1421
1422 /**
1423 * Determine if the given model has a scope.
1424 *
1425 * @param string $scope
1426 * @return bool
1427 */
1428 public function hasNamedScope($scope)
1429 {
1430 return $this->model && $this->model->hasNamedScope($scope);
1431 }
1432
1433 /**
1434 * Call the given local model scopes.
1435 *
1436 * @param array|string $scopes
1437 * @return static|mixed
1438 */
1439 public function scopes($scopes)
1440 {
1441 $builder = $this;
1442
1443 foreach (Arr::wrap($scopes) as $scope => $parameters) {
1444 // If the scope key is an integer, then the scope was passed as the value and
1445 // the parameter list is empty, so we will format the scope name and these
1446 // parameters here. Then, we'll be ready to call the scope on the model.
1447 if (is_int($scope)) {
1448 [$scope, $parameters] = [$parameters, []];
1449 }
1450
1451 // Next we'll pass the scope callback to the callScope method which will take
1452 // care of grouping the "wheres" properly so the logical order doesn't get
1453 // messed up when adding scopes. Then we'll return back out the builder.
1454 $builder = $builder->callNamedScope(
1455 $scope, Arr::wrap($parameters)
1456 );
1457 }
1458
1459 return $builder;
1460 }
1461
1462 /**
1463 * Apply the scopes to the Orm builder instance and return it.
1464 *
1465 * @return static
1466 */
1467 public function applyScopes()
1468 {
1469 if (! $this->scopes) {
1470 return $this;
1471 }
1472
1473 $builder = clone $this;
1474
1475 foreach ($this->scopes as $identifier => $scope) {
1476 if (! isset($builder->scopes[$identifier])) {
1477 continue;
1478 }
1479
1480 $builder->callScope(function (self $builder) use ($scope) {
1481 // If the scope is a Closure we will just go ahead and call the scope with the
1482 // builder instance. The "callScope" method will properly group the clauses
1483 // that are added to this query so "where" clauses maintain proper logic.
1484 if ($scope instanceof Closure) {
1485 $scope($builder);
1486 }
1487
1488 // If the scope is a scope object, we will call the apply method on this scope
1489 // passing in the builder and the model instance. After we run all of these
1490 // scopes we will return back the builder instance to the outside caller.
1491 if ($scope instanceof Scope) {
1492 $scope->apply($builder, $this->getModel());
1493 }
1494 });
1495 }
1496
1497 return $builder;
1498 }
1499
1500 /**
1501 * Apply the given scope on the current builder instance.
1502 *
1503 * @param callable $scope
1504 * @param array $parameters
1505 * @return mixed
1506 */
1507 protected function callScope(callable $scope, array $parameters = [])
1508 {
1509 array_unshift($parameters, $this);
1510
1511 $query = $this->getQuery();
1512
1513 // We will keep track of how many wheres are on the query before running the
1514 // scope so that we can properly group the added scope constraints in the
1515 // query as their own isolated nested where statement and avoid issues.
1516 $originalWhereCount = is_null($query->wheres)
1517 ? 0 : count($query->wheres);
1518
1519 $result = $scope(...$parameters) ?? $this;
1520
1521 if (count((array) $query->wheres) > $originalWhereCount) {
1522 $this->addNewWheresWithinGroup($query, $originalWhereCount);
1523 }
1524
1525 return $result;
1526 }
1527
1528 /**
1529 * Apply the given named scope on the current builder instance.
1530 *
1531 * @param string $scope
1532 * @param array $parameters
1533 * @return mixed
1534 */
1535 protected function callNamedScope($scope, array $parameters = [])
1536 {
1537 return $this->callScope(function (...$parameters) use ($scope) {
1538 return $this->model->callNamedScope($scope, $parameters);
1539 }, $parameters);
1540 }
1541
1542 /**
1543 * Nest where conditions by slicing them at the given where count.
1544 *
1545 * @param \FluentBoards\Framework\Database\Query\Builder $query
1546 * @param int $originalWhereCount
1547 * @return void
1548 */
1549 protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
1550 {
1551 // Here, we totally remove all of the where clauses since we are going to
1552 // rebuild them as nested queries by slicing the groups of wheres into
1553 // their own sections. This is to prevent any confusing logic order.
1554 $allWheres = $query->wheres;
1555
1556 $query->wheres = [];
1557
1558 $this->groupWhereSliceForScope(
1559 $query, array_slice($allWheres, 0, $originalWhereCount)
1560 );
1561
1562 $this->groupWhereSliceForScope(
1563 $query, array_slice($allWheres, $originalWhereCount)
1564 );
1565 }
1566
1567 /**
1568 * Slice where conditions at the given offset and add them to the query as a nested condition.
1569 *
1570 * @param \FluentBoards\Framework\Database\Query\Builder $query
1571 * @param array $whereSlice
1572 * @return void
1573 */
1574 protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
1575 {
1576 $whereBooleans = Collection::make($whereSlice)->pluck('boolean');
1577
1578 // Here we'll check if the given subset of where clauses contains any "or"
1579 // booleans and in this case create a nested where expression. That way
1580 // we don't add any unnecessary nesting thus keeping the query clean.
1581 if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) {
1582 $query->wheres[] = $this->createNestedWhere(
1583 $whereSlice, str_replace(' not', '', $whereBooleans->first())
1584 );
1585 } else {
1586 $query->wheres = array_merge($query->wheres, $whereSlice);
1587 }
1588 }
1589
1590 /**
1591 * Create a where array with nested where conditions.
1592 *
1593 * @param array $whereSlice
1594 * @param string $boolean
1595 * @return array
1596 */
1597 protected function createNestedWhere($whereSlice, $boolean = 'and')
1598 {
1599 $whereGroup = $this->getQuery()->forNestedWhere();
1600
1601 $whereGroup->wheres = $whereSlice;
1602
1603 return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
1604 }
1605
1606 /**
1607 * Set the relationships that should be eager loaded.
1608 *
1609 * @param string|array $relations
1610 * @param string|\Closure|null $callback
1611 * @return $this
1612 */
1613 public function with($relations, $callback = null)
1614 {
1615 if ($callback instanceof Closure) {
1616 $eagerLoad = $this->parseWithRelations([$relations => $callback]);
1617 } else {
1618 $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
1619 }
1620
1621 $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
1622
1623 return $this;
1624 }
1625
1626 /**
1627 * Prevent the specified relations from being eager loaded.
1628 *
1629 * @param mixed $relations
1630 * @return $this
1631 */
1632 public function without($relations)
1633 {
1634 $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip(
1635 is_string($relations) ? func_get_args() : $relations
1636 ));
1637
1638 return $this;
1639 }
1640
1641 /**
1642 * Set the relationships that should be eager loaded while removing any previously added eager loading specifications.
1643 *
1644 * @param mixed $relations
1645 * @return $this
1646 */
1647 public function withOnly($relations)
1648 {
1649 $this->eagerLoad = [];
1650
1651 return $this->with($relations);
1652 }
1653
1654 /**
1655 * Create a new instance of the model being queried.
1656 *
1657 * @param array $attributes
1658 * @return \FluentBoards\Framework\Database\Orm\Model|static
1659 */
1660 public function newModelInstance($attributes = [])
1661 {
1662 return $this->model->newInstance($attributes)->setConnection(
1663 $this->query->getConnection()->getName()
1664 );
1665 }
1666
1667 /**
1668 * Parse a list of relations into individuals.
1669 *
1670 * @param array $relations
1671 * @return array
1672 */
1673 protected function parseWithRelations(array $relations)
1674 {
1675 if ($relations === []) {
1676 return [];
1677 }
1678
1679 $results = [];
1680
1681 foreach ($this->prepareNestedWithRelationships($relations) as $name => $constraints) {
1682 // We need to separate out any nested includes, which allows the developers
1683 // to load deep relationships using "dots" without stating each level of
1684 // the relationship with its own key in the array of eager-load names.
1685 $results = $this->addNestedWiths($name, $results);
1686
1687 $results[$name] = $constraints;
1688 }
1689
1690 return $results;
1691 }
1692
1693 protected function prepareNestedWithRelationships($relations, $prefix = '')
1694 {
1695 $preparedRelationships = [];
1696
1697 if ($prefix !== '') {
1698 $prefix .= '.';
1699 }
1700
1701 // If any of the relationships are formatted with the [$attribute => array()]
1702 // syntax, we shall loop over the nested relations and prepend each key of
1703 // this array while flattening into the traditional dot notation format.
1704 foreach ($relations as $key => $value) {
1705 if (! is_string($key) || ! is_array($value)) {
1706 continue;
1707 }
1708
1709 [$attribute, $attributeSelectConstraint] = $this->parseNameAndAttributeSelectionConstraint($key);
1710
1711 $preparedRelationships = array_merge(
1712 $preparedRelationships,
1713 ["{$prefix}{$attribute}" => $attributeSelectConstraint],
1714 $this->prepareNestedWithRelationships($value, "{$prefix}{$attribute}"),
1715 );
1716
1717 unset($relations[$key]);
1718 }
1719
1720 // We now know that the remaining relationships are in a dot notation format
1721 // and may be a string or Closure. We'll loop over them and ensure all of
1722 // the present Closures are merged + strings are made into constraints.
1723 foreach ($relations as $key => $value) {
1724 if (is_numeric($key) && is_string($value)) {
1725 [$key, $value] = $this->parseNameAndAttributeSelectionConstraint($value);
1726 }
1727
1728 $preparedRelationships[$prefix.$key] = $this->combineConstraints([
1729 $value,
1730 $preparedRelationships[$prefix.$key] ?? static function () {
1731 //
1732 },
1733 ]);
1734 }
1735
1736 return $preparedRelationships;
1737 }
1738
1739 /**
1740 * Combine an array of constraints into a single constraint.
1741 *
1742 * @param array $constraints
1743 * @return \Closure
1744 */
1745 protected function combineConstraints(array $constraints)
1746 {
1747 return function ($builder) use ($constraints) {
1748 foreach ($constraints as $constraint) {
1749 $builder = $constraint($builder) ?? $builder;
1750 }
1751
1752 return $builder;
1753 };
1754 }
1755
1756 /**
1757 * Parse the attribute select constraints from the name.
1758 *
1759 * @param string $name
1760 * @return array
1761 */
1762 protected function parseNameAndAttributeSelectionConstraint($name)
1763 {
1764 return str_contains($name, ':')
1765 ? $this->createSelectWithConstraint($name)
1766 : [$name, static function () {
1767 //
1768 }];
1769 }
1770
1771 /**
1772 * Create a constraint to select the given columns for the relation.
1773 *
1774 * @param string $name
1775 * @return array
1776 */
1777 protected function createSelectWithConstraint($name)
1778 {
1779 return [explode(':', $name)[0], static function ($query) use ($name) {
1780 $query->select(array_map(static function ($column) use ($query) {
1781 if (Str::contains($column, '.')) {
1782 return $column;
1783 }
1784
1785 return $query instanceof BelongsToMany
1786 ? $query->getRelated()->getTable().'.'.$column
1787 : $column;
1788 }, explode(',', explode(':', $name)[1])));
1789 }];
1790 }
1791
1792 /**
1793 * Parse the nested relationships in a relation.
1794 *
1795 * @param string $name
1796 * @param array $results
1797 * @return array
1798 */
1799 protected function addNestedWiths($name, $results)
1800 {
1801 $progress = [];
1802
1803 // If the relation has already been set on the result array, we will not set it
1804 // again, since that would override any constraints that were already placed
1805 // on the relationships. We will only set the ones that are not specified.
1806 foreach (explode('.', $name) as $segment) {
1807 $progress[] = $segment;
1808
1809 if (! isset($results[$last = implode('.', $progress)])) {
1810 $results[$last] = static function () {
1811 //
1812 };
1813 }
1814 }
1815
1816 return $results;
1817 }
1818
1819 /**
1820 * Apply query-time casts to the model instance.
1821 *
1822 * @param array $casts
1823 * @return $this
1824 */
1825 public function withCasts($casts)
1826 {
1827 $this->model->mergeCasts($casts);
1828
1829 return $this;
1830 }
1831
1832 /**
1833 * Execute the given Closure within a transaction savepoint if needed.
1834 *
1835 * @template TModelValue
1836 *
1837 * @param \Closure(): TModelValue $scope
1838 * @return TModelValue
1839 */
1840 public function withSavepointIfNeeded(Closure $scope)
1841 {
1842 return $this->getQuery()->getConnection()->transactionLevel() > 0
1843 ? $this->getQuery()->getConnection()->transaction($scope)
1844 : $scope();
1845 }
1846
1847 /**
1848 * Get the Eloquent builder instances that are used in the union of the query.
1849 *
1850 * @return \FluentBoards\Framework\Support\Collection
1851 */
1852 protected function getUnionBuilders()
1853 {
1854 return isset($this->query->unions)
1855 ? Helper::collect($this->query->unions)->pluck('query')
1856 : Helper::collect();
1857 }
1858
1859 /**
1860 * Get the underlying query builder instance.
1861 *
1862 * @return \FluentBoards\Framework\Database\Query\Builder
1863 */
1864 public function getQuery()
1865 {
1866 return $this->query;
1867 }
1868
1869 /**
1870 * Set the underlying query builder instance.
1871 *
1872 * @param \FluentBoards\Framework\Database\Query\Builder $query
1873 * @return $this
1874 */
1875 public function setQuery($query)
1876 {
1877 $this->query = $query;
1878
1879 return $this;
1880 }
1881
1882 /**
1883 * Get a base query builder instance.
1884 *
1885 * @return \FluentBoards\Framework\Database\Query\Builder
1886 */
1887 public function toBase()
1888 {
1889 return $this->applyScopes()->getQuery();
1890 }
1891
1892 /**
1893 * Get the relationships being eagerly loaded.
1894 *
1895 * @return array
1896 */
1897 public function getEagerLoads()
1898 {
1899 return $this->eagerLoad;
1900 }
1901
1902 /**
1903 * Set the relationships being eagerly loaded.
1904 *
1905 * @param array $eagerLoad
1906 * @return $this
1907 */
1908 public function setEagerLoads(array $eagerLoad)
1909 {
1910 $this->eagerLoad = $eagerLoad;
1911
1912 return $this;
1913 }
1914
1915 /**
1916 * Indicate that the given relationships should not be eagerly loaded.
1917 *
1918 * @param array $relations
1919 * @return $this
1920 */
1921 public function withoutEagerLoad(array $relations)
1922 {
1923 $relations = array_diff(array_keys($this->model->getRelations()), $relations);
1924
1925 return $this->with($relations);
1926 }
1927
1928 /**
1929 * Flush the relationships being eagerly loaded.
1930 *
1931 * @return $this
1932 */
1933 public function withoutEagerLoads()
1934 {
1935 return $this->setEagerLoads([]);
1936 }
1937
1938 /**
1939 * Get the default key name of the table.
1940 *
1941 * @return string
1942 */
1943 protected function defaultKeyName()
1944 {
1945 return $this->getModel()->getKeyName();
1946 }
1947
1948 /**
1949 * Get the model instance being queried.
1950 *
1951 * @return \FluentBoards\Framework\Database\Orm\Model|static
1952 */
1953 public function getModel()
1954 {
1955 return $this->model;
1956 }
1957
1958 /**
1959 * Set a model instance for the model being queried.
1960 *
1961 * @param \FluentBoards\Framework\Database\Orm\Model $model
1962 * @return $this
1963 */
1964 public function setModel(Model $model)
1965 {
1966 $this->model = $model;
1967
1968 $this->query->from($model->getTable());
1969
1970 return $this;
1971 }
1972
1973 /**
1974 * Qualify the given column name by the model's table.
1975 *
1976 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1977 * @return string
1978 */
1979 public function qualifyColumn($column)
1980 {
1981 $column = $column instanceof Expression ? $column->getValue(
1982 $this->getGrammar()
1983 ) : $column;
1984
1985 return $this->model->qualifyColumn($column);
1986 }
1987
1988 /**
1989 * Qualify the given columns with the model's table.
1990 *
1991 * @param array|\FluentBoards\Framework\Database\Query\Expression $columns
1992 * @return array
1993 */
1994 public function qualifyColumns($columns)
1995 {
1996 return $this->model->qualifyColumns($columns);
1997 }
1998
1999 /**
2000 * Get the given macro by name.
2001 *
2002 * @param string $name
2003 * @return \Closure
2004 */
2005 public function getMacro($name)
2006 {
2007 return Arr::get($this->localMacros, $name);
2008 }
2009
2010 /**
2011 * Checks if a macro is registered.
2012 *
2013 * @param string $name
2014 * @return bool
2015 */
2016 public function hasMacro($name)
2017 {
2018 return isset($this->localMacros[$name]);
2019 }
2020
2021 /**
2022 * Get the given global macro by name.
2023 *
2024 * @param string $name
2025 * @return \Closure
2026 */
2027 public static function getGlobalMacro($name)
2028 {
2029 return Arr::get(static::$macros, $name);
2030 }
2031
2032 /**
2033 * Checks if a global macro is registered.
2034 *
2035 * @param string $name
2036 * @return bool
2037 */
2038 public static function hasGlobalMacro($name)
2039 {
2040 return isset(static::$macros[$name]);
2041 }
2042
2043 /**
2044 * Dynamically access builder proxies.
2045 *
2046 * @param string $key
2047 * @return mixed
2048 *
2049 * @throws \Exception
2050 */
2051 public function __get($key)
2052 {
2053 if (in_array($key, ['orWhere', 'whereNot', 'orWhereNot'])) {
2054 return new HigherOrderBuilderProxy($this, $key);
2055 }
2056
2057 if (in_array($key, $this->propertyPassthru)) {
2058 return $this->toBase()->{$key};
2059 }
2060
2061 throw new Exception("Property [{$key}] does not exist on the Eloquent builder instance.");
2062 }
2063
2064 /**
2065 * Dynamically handle calls into the query instance.
2066 *
2067 * @param string $method
2068 * @param array $parameters
2069 * @return mixed
2070 */
2071 public function __call($method, $parameters)
2072 {
2073 if ($method === 'macro') {
2074 $this->localMacros[$parameters[0]] = $parameters[1];
2075
2076 return;
2077 }
2078
2079 if ($this->hasMacro($method)) {
2080 array_unshift($parameters, $this);
2081
2082 return $this->localMacros[$method](...$parameters);
2083 }
2084
2085 if (static::hasGlobalMacro($method)) {
2086 $callable = static::$macros[$method];
2087
2088 if ($callable instanceof Closure) {
2089 $callable = $callable->bindTo($this, static::class);
2090 }
2091
2092 return $callable(...$parameters);
2093 }
2094
2095 if ($this->hasNamedScope($method)) {
2096 return $this->callNamedScope($method, $parameters);
2097 }
2098
2099 if (in_array(strtolower($method), $this->passthru)) {
2100 return $this->toBase()->{$method}(...$parameters);
2101 }
2102
2103 $this->forwardCallTo($this->query, $method, $parameters);
2104
2105 return $this;
2106 }
2107
2108 /**
2109 * Dynamically handle calls into the query instance.
2110 *
2111 * @param string $method
2112 * @param array $parameters
2113 * @return mixed
2114 *
2115 * @throws \BadMethodCallException
2116 */
2117 public static function __callStatic($method, $parameters)
2118 {
2119 if ($method === 'macro') {
2120 static::$macros[$parameters[0]] = $parameters[1];
2121
2122 return;
2123 }
2124
2125 if ($method === 'mixin') {
2126 return static::registerMixin($parameters[0], $parameters[1] ?? true);
2127 }
2128
2129 if (! static::hasGlobalMacro($method)) {
2130 static::throwBadMethodCallException($method);
2131 }
2132
2133 $callable = static::$macros[$method];
2134
2135 if ($callable instanceof Closure) {
2136 $callable = $callable->bindTo(null, static::class);
2137 }
2138
2139 return $callable(...$parameters);
2140 }
2141
2142 /**
2143 * Register the given mixin with the builder.
2144 *
2145 * @param string $mixin
2146 * @param bool $replace
2147 * @return void
2148 */
2149 protected static function registerMixin($mixin, $replace)
2150 {
2151 $methods = (new ReflectionClass($mixin))->getMethods(
2152 ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
2153 );
2154
2155 foreach ($methods as $method) {
2156 if ($replace || ! static::hasGlobalMacro($method->name)) {
2157 static::macro($method->name, $method->invoke($mixin));
2158 }
2159 }
2160 }
2161
2162 /**
2163 * Clone the Orm query builder.
2164 *
2165 * @return static
2166 */
2167 public function clone()
2168 {
2169 return clone $this;
2170 }
2171
2172 /**
2173 * Force a clone of the underlying query builder when cloning.
2174 *
2175 * @return void
2176 */
2177 public function __clone()
2178 {
2179 $this->query = clone $this->query;
2180 }
2181 }
2182