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

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