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

1,731 lines 47.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\Concerns\BuildsQueries;
17 use FluentBoards\Framework\Database\RecordsNotFoundException;
18 use FluentBoards\Framework\Database\Orm\Relations\Relation;
19 use FluentBoards\Framework\Database\Orm\Relations\BelongsToMany;
20 use FluentBoards\Framework\Database\Orm\RelationNotFoundException;
21 use FluentBoards\Framework\Database\Orm\Concerns\QueriesRelationships;
22 use FluentBoards\Framework\Database\Query\Builder as QueryBuilder;
23
24
25 /**
26 * @property-read HigherOrderBuilderProxy $orWhere
27 *
28 * @mixin \FluentBoards\Framework\Database\Query\Builder
29 */
30 class Builder
31 {
32 use QueriesRelationships, ForwardsCalls;
33 use BuildsQueries {
34 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 'doesntExist',
100 'dump',
101 'exists',
102 'explain',
103 'getBindings',
104 'getConnection',
105 'getGrammar',
106 'insert',
107 'insertGetId',
108 'insertOrIgnore',
109 'insertUsing',
110 'max',
111 'min',
112 'raw',
113 'sum',
114 'toSql',
115 ];
116
117 /**
118 * Applied global scopes.
119 *
120 * @var array
121 */
122 protected $scopes = [];
123
124 /**
125 * Removed global scopes.
126 *
127 * @var array
128 */
129 protected $removedScopes = [];
130
131 /**
132 * Create a new Orm query builder instance.
133 *
134 * @param \FluentBoards\Framework\Database\Query\Builder $query
135 * @return void
136 */
137 public function __construct(QueryBuilder $query)
138 {
139 $this->query = $query;
140 }
141
142 /**
143 * Create and return an un-saved model instance.
144 *
145 * @param array $attributes
146 * @return \FluentBoards\Framework\Database\Orm\Model|static
147 */
148 public function make(array $attributes = [])
149 {
150 return $this->newModelInstance($attributes);
151 }
152
153 /**
154 * Register a new global scope.
155 *
156 * @param string $identifier
157 * @param \FluentBoards\Framework\Database\Orm\Scope|\Closure $scope
158 * @return $this
159 */
160 public function withGlobalScope($identifier, $scope)
161 {
162 $this->scopes[$identifier] = $scope;
163
164 if (method_exists($scope, 'extend')) {
165 $scope->extend($this);
166 }
167
168 return $this;
169 }
170
171 /**
172 * Remove a registered global scope.
173 *
174 * @param \FluentBoards\Framework\Database\Orm\Scope|string $scope
175 * @return $this
176 */
177 public function withoutGlobalScope($scope)
178 {
179 if (! is_string($scope)) {
180 $scope = get_class($scope);
181 }
182
183 unset($this->scopes[$scope]);
184
185 $this->removedScopes[] = $scope;
186
187 return $this;
188 }
189
190 /**
191 * Remove all or passed registered global scopes.
192 *
193 * @param array|null $scopes
194 * @return $this
195 */
196 public function withoutGlobalScopes(array $scopes = null)
197 {
198 if (! is_array($scopes)) {
199 $scopes = array_keys($this->scopes);
200 }
201
202 foreach ($scopes as $scope) {
203 $this->withoutGlobalScope($scope);
204 }
205
206 return $this;
207 }
208
209 /**
210 * Get an array of global scopes that were removed from the query.
211 *
212 * @return array
213 */
214 public function removedScopes()
215 {
216 return $this->removedScopes;
217 }
218
219 /**
220 * Add a where clause on the primary key to the query.
221 *
222 * @param mixed $id
223 * @return $this
224 */
225 public function whereKey($id)
226 {
227 if ($id instanceof Model) {
228 $id = $id->getKey();
229 }
230
231 if (is_array($id) || $id instanceof ArrayableInterface) {
232 $this->query->whereIn($this->model->getQualifiedKeyName(), $id);
233
234 return $this;
235 }
236
237 if ($id !== null && $this->model->getKeyType() === 'string') {
238 $id = (string) $id;
239 }
240
241 return $this->where($this->model->getQualifiedKeyName(), '=', $id);
242 }
243
244 /**
245 * Add a where clause on the primary key to the query.
246 *
247 * @param mixed $id
248 * @return $this
249 */
250 public function whereKeyNot($id)
251 {
252 if ($id instanceof Model) {
253 $id = $id->getKey();
254 }
255
256 if (is_array($id) || $id instanceof ArrayableInterface) {
257 $this->query->whereNotIn($this->model->getQualifiedKeyName(), $id);
258
259 return $this;
260 }
261
262 if ($id !== null && $this->model->getKeyType() === 'string') {
263 $id = (string) $id;
264 }
265
266 return $this->where($this->model->getQualifiedKeyName(), '!=', $id);
267 }
268
269 /**
270 * Add a basic where clause to the query.
271 *
272 * @param \Closure|string|array|\FluentBoards\Framework\Database\Query\Expression $column
273 * @param mixed $operator
274 * @param mixed $value
275 * @param string $boolean
276 * @return $this
277 */
278 public function where($column, $operator = null, $value = null, $boolean = 'and')
279 {
280 if ($column instanceof Closure && is_null($operator)) {
281 $column($query = $this->model->newQueryWithoutRelationships());
282
283 $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
284 } else {
285 $this->query->where(...func_get_args());
286 }
287
288 return $this;
289 }
290
291 /**
292 * Add a basic where clause to the query, and return the first result.
293 *
294 * @param \Closure|string|array|\FluentBoards\Framework\Database\Query\Expression $column
295 * @param mixed $operator
296 * @param mixed $value
297 * @param string $boolean
298 * @return \FluentBoards\Framework\Database\Orm\Model|static|null
299 */
300 public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
301 {
302 return $this->where($column, $operator, $value, $boolean)->first();
303 }
304
305 /**
306 * Add an "or where" clause to the query.
307 *
308 * @param \Closure|array|string|\FluentBoards\Framework\Database\Query\Expression $column
309 * @param mixed $operator
310 * @param mixed $value
311 * @return $this
312 */
313 public function orWhere($column, $operator = null, $value = null)
314 {
315 [$value, $operator] = $this->query->prepareValueAndOperator(
316 $value, $operator, func_num_args() === 2
317 );
318
319 return $this->where($column, $operator, $value, 'or');
320 }
321
322 /**
323 * Add an "order by" clause for a timestamp to the query.
324 *
325 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
326 * @return $this
327 */
328 public function latest($column = null)
329 {
330 if (is_null($column)) {
331 $column = $this->model->getCreatedAtColumn() ?? 'created_at';
332 }
333
334 $this->query->latest($column);
335
336 return $this;
337 }
338
339 /**
340 * Add an "order by" clause for a timestamp to the query.
341 *
342 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
343 * @return $this
344 */
345 public function oldest($column = null)
346 {
347 if (is_null($column)) {
348 $column = $this->model->getCreatedAtColumn() ?? 'created_at';
349 }
350
351 $this->query->oldest($column);
352
353 return $this;
354 }
355
356 /**
357 * Create a collection of models from plain arrays.
358 *
359 * @param array $items
360 * @return \FluentBoards\Framework\Database\Orm\Collection
361 */
362 public function hydrate(array $items)
363 {
364 $instance = $this->newModelInstance();
365
366 return $instance->newCollection(array_map(function ($item) use ($items, $instance) {
367 $model = $instance->newFromBuilder($item);
368
369 if (count($items) > 1) {
370 $model->preventsLazyLoading = Model::preventsLazyLoading();
371 }
372
373 return $model;
374 }, $items));
375 }
376
377 /**
378 * Create a collection of models from a raw query.
379 *
380 * @param string $query
381 * @param array $bindings
382 * @return \FluentBoards\Framework\Database\Orm\Collection
383 */
384 public function fromQuery($query, $bindings = [])
385 {
386 return $this->hydrate(
387 $this->query->getConnection()->select($query, $bindings)
388 );
389 }
390
391 /**
392 * Find a model by its primary key.
393 *
394 * @param mixed $id
395 * @param array $columns
396 * @return \FluentBoards\Framework\Database\Orm\Model|\FluentBoards\Framework\Database\Orm\Collection|static[]|static|null
397 */
398 public function find($id, $columns = ['*'])
399 {
400 if (is_array($id) || $id instanceof ArrayableInterface) {
401 return $this->findMany($id, $columns);
402 }
403
404 return $this->whereKey($id)->first($columns);
405 }
406
407 /**
408 * Find multiple models by their primary keys.
409 *
410 * @param \FluentBoards\Framework\Support\ArrayableInterface|array $ids
411 * @param array $columns
412 * @return \FluentBoards\Framework\Database\Orm\Collection
413 */
414 public function findMany($ids, $columns = ['*'])
415 {
416 $ids = $ids instanceof ArrayableInterface ? $ids->toArray() : $ids;
417
418 if (empty($ids)) {
419 return $this->model->newCollection();
420 }
421
422 return $this->whereKey($ids)->get($columns);
423 }
424
425 /**
426 * Find a model by its primary key or throw an exception.
427 *
428 * @param mixed $id
429 * @param array $columns
430 * @return \FluentBoards\Framework\Database\Orm\Model|\FluentBoards\Framework\Database\Orm\Collection|static|static[]
431 *
432 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException
433 */
434 public function findOrFail($id, $columns = ['*'])
435 {
436 $result = $this->find($id, $columns);
437
438 $id = $id instanceof ArrayableInterface ? $id->toArray() : $id;
439
440 if (is_array($id)) {
441 if (count($result) === count(array_unique($id))) {
442 return $result;
443 }
444 } elseif (! is_null($result)) {
445 return $result;
446 }
447
448 throw (new ModelNotFoundException)->setModel(
449 get_class($this->model), $id
450 );
451 }
452
453 /**
454 * Find a model by its primary key or return fresh model instance.
455 *
456 * @param mixed $id
457 * @param array $columns
458 * @return \FluentBoards\Framework\Database\Orm\Model|static
459 */
460 public function findOrNew($id, $columns = ['*'])
461 {
462 if (! is_null($model = $this->find($id, $columns))) {
463 return $model;
464 }
465
466 return $this->newModelInstance();
467 }
468
469 /**
470 * Get the first record matching the attributes or instantiate it.
471 *
472 * @param array $attributes
473 * @param array $values
474 * @return \FluentBoards\Framework\Database\Orm\Model|static
475 */
476 public function firstOrNew(array $attributes = [], array $values = [])
477 {
478 if (! is_null($instance = $this->where($attributes)->first())) {
479 return $instance;
480 }
481
482 return $this->newModelInstance(array_merge($attributes, $values));
483 }
484
485 /**
486 * Get the first record matching the attributes or create it.
487 *
488 * @param array $attributes
489 * @param array $values
490 * @return \FluentBoards\Framework\Database\Orm\Model|static
491 */
492 public function firstOrCreate(array $attributes = [], array $values = [])
493 {
494 if (! is_null($instance = $this->where($attributes)->first())) {
495 return $instance;
496 }
497
498 return Helper::tap($this->newModelInstance(array_merge($attributes, $values)), function ($instance) {
499 $instance->save();
500 });
501 }
502
503 /**
504 * Create or update a record matching the attributes, and fill it with values.
505 *
506 * @param array $attributes
507 * @param array $values
508 * @return \FluentBoards\Framework\Database\Orm\Model|static
509 */
510 public function updateOrCreate(array $attributes, array $values = [])
511 {
512 return Helper::tap($this->firstOrNew($attributes), function ($instance) use ($values) {
513 $instance->fill($values)->save();
514 });
515 }
516
517 /**
518 * Execute the query and get the first result or throw an exception.
519 *
520 * @param array $columns
521 * @return \FluentBoards\Framework\Database\Orm\Model|static
522 *
523 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException
524 */
525 public function firstOrFail($columns = ['*'])
526 {
527 if (! is_null($model = $this->first($columns))) {
528 return $model;
529 }
530
531 throw (new ModelNotFoundException)->setModel(get_class($this->model));
532 }
533
534 /**
535 * Execute the query and get the first result or call a callback.
536 *
537 * @param \Closure|array $columns
538 * @param \Closure|null $callback
539 * @return \FluentBoards\Framework\Database\Orm\Model|static|mixed
540 */
541 public function firstOr($columns = ['*'], Closure $callback = null)
542 {
543 if ($columns instanceof Closure) {
544 $callback = $columns;
545
546 $columns = ['*'];
547 }
548
549 if (! is_null($model = $this->first($columns))) {
550 return $model;
551 }
552
553 return $callback();
554 }
555
556 /**
557 * Execute the query and get the first result if it's the sole matching record.
558 *
559 * @param array|string $columns
560 * @return \FluentBoards\Framework\Database\Orm\Model
561 *
562 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException
563 * @throws \FluentBoards\Framework\Database\MultipleRecordsFoundException
564 */
565 public function sole($columns = ['*'])
566 {
567 try {
568 return $this->baseSole($columns);
569 } catch (RecordsNotFoundException $exception) {
570 throw (new ModelNotFoundException)->setModel(get_class($this->model));
571 }
572 }
573
574 /**
575 * Get a single column's value from the first result of a query.
576 *
577 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
578 * @return mixed
579 */
580 public function value($column)
581 {
582 if ($result = $this->first([$column])) {
583 return $result->{Str::afterLast($column, '.')};
584 }
585 }
586
587 /**
588 * Get a single column's value from the first result of the query or throw an exception.
589 *
590 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
591 * @return mixed
592 *
593 * @throws \FluentBoards\Framework\Database\Orm\ModelNotFoundException
594 */
595 public function valueOrFail($column)
596 {
597 return $this->firstOrFail([$column])->{Str::afterLast($column, '.')};
598 }
599
600 /**
601 * Execute the query as a "select" statement.
602 *
603 * @param array|string $columns
604 * @return \FluentBoards\Framework\Database\Orm\Collection|static[]
605 */
606 public function get($columns = ['*'])
607 {
608 $builder = $this->applyScopes();
609
610 // If we actually found models we will also eager load any relationships that
611 // have been specified as needing to be eager loaded, which will solve the
612 // n+1 query issue for the developers to avoid running a lot of queries.
613 if (count($models = $builder->getModels($columns)) > 0) {
614 $models = $builder->eagerLoadRelations($models);
615 }
616
617 return $builder->getModel()->newCollection($models);
618 }
619
620 /**
621 * Get the hydrated models without eager loading.
622 *
623 * @param array|string $columns
624 * @return \FluentBoards\Framework\Database\Orm\Model[]|static[]
625 */
626 public function getModels($columns = ['*'])
627 {
628 return $this->model->hydrate(
629 $this->query->get($columns)->all()
630 )->all();
631 }
632
633 /**
634 * Eager load the relationships for the models.
635 *
636 * @param array $models
637 * @return array
638 */
639 public function eagerLoadRelations(array $models)
640 {
641 foreach ($this->eagerLoad as $name => $constraints) {
642 // For nested eager loads we'll skip loading them here and they will be set as an
643 // eager load on the query to retrieve the relation so that they will be eager
644 // loaded on that query, because that is where they get hydrated as models.
645 if (strpos($name, '.') === false) {
646 $models = $this->eagerLoadRelation($models, $name, $constraints);
647 }
648 }
649
650 return $models;
651 }
652
653 /**
654 * Eagerly load the relationship on a set of models.
655 *
656 * @param array $models
657 * @param string $name
658 * @param \Closure $constraints
659 * @return array
660 */
661 protected function eagerLoadRelation(array $models, $name, Closure $constraints)
662 {
663 // First we will "back up" the existing where conditions on the query so we can
664 // add our eager constraints. Then we will merge the wheres that were on the
665 // query back to it in order that any where conditions might be specified.
666 $relation = $this->getRelation($name);
667
668 $relation->addEagerConstraints($models);
669
670 $constraints($relation);
671
672 // Once we have the results, we just match those back up to their parent models
673 // using the relationship instance. Then we just return the finished arrays
674 // of models which have been eagerly hydrated and are readied for return.
675 return $relation->match(
676 $relation->initRelation($models, $name),
677 $relation->getEager(), $name
678 );
679 }
680
681 /**
682 * Get the relation instance for the given relation name.
683 *
684 * @param string $name
685 * @return \FluentBoards\Framework\Database\Orm\Relations\Relation
686 */
687 public function getRelation($name)
688 {
689 // We want to run a relationship query without any constrains so that we will
690 // not have to remove these where clauses manually which gets really hacky
691 // and error prone. We don't want constraints because we add eager ones.
692 $relation = Relation::noConstraints(function () use ($name) {
693 try {
694 return $this->getModel()->newInstance()->$name();
695 } catch (BadMethodCallException $e) {
696 throw RelationNotFoundException::make($this->getModel(), $name);
697 }
698 });
699
700 $nested = $this->relationsNestedUnder($name);
701
702 // If there are nested relationships set on the query, we will put those onto
703 // the query instances so that they can be handled after this relationship
704 // is loaded. In this way they will all trickle down as they are loaded.
705 if (count($nested) > 0) {
706 $relation->getQuery()->with($nested);
707 }
708
709 return $relation;
710 }
711
712 /**
713 * Get the deeply nested relations for a given top-level relation.
714 *
715 * @param string $relation
716 * @return array
717 */
718 protected function relationsNestedUnder($relation)
719 {
720 $nested = [];
721
722 // We are basically looking for any relationships that are nested deeper than
723 // the given top-level relationship. We will just check for any relations
724 // that start with the given top relations and adds them to our arrays.
725 foreach ($this->eagerLoad as $name => $constraints) {
726 if ($this->isNestedUnder($relation, $name)) {
727 $nested[substr($name, strlen($relation.'.'))] = $constraints;
728 }
729 }
730
731 return $nested;
732 }
733
734 /**
735 * Determine if the relationship is nested.
736 *
737 * @param string $relation
738 * @param string $name
739 * @return bool
740 */
741 protected function isNestedUnder($relation, $name)
742 {
743 return Str::contains($name, '.') && Str::startsWith($name, $relation.'.');
744 }
745
746 /**
747 * Get a lazy collection for the given query.
748 *
749 * @return \FluentBoards\Framework\Support\LazyCollection
750 */
751 public function cursor()
752 {
753 return $this->applyScopes()->query->cursor()->map(function ($record) {
754 return $this->newModelInstance()->newFromBuilder($record);
755 });
756 }
757
758 /**
759 * Add a generic "order by" clause if the query doesn't already have one.
760 *
761 * @return void
762 */
763 protected function enforceOrderBy()
764 {
765 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
766 $this->orderBy($this->model->getQualifiedKeyName(), 'asc');
767 }
768 }
769
770 /**
771 * Get an array with the values of a given column.
772 *
773 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
774 * @param string|null $key
775 * @return \FluentBoards\Framework\Support\Collection
776 */
777 public function pluck($column, $key = null)
778 {
779 $results = $this->toBase()->pluck($column, $key);
780
781 // If the model has a mutator for the requested column, we will spin through
782 // the results and mutate the values so that the mutated version of these
783 // columns are returned as you would expect from these Orm models.
784 if (! $this->model->hasGetMutator($column) &&
785 ! $this->model->hasCast($column) &&
786 ! in_array($column, $this->model->getDates())) {
787 return $results;
788 }
789
790 return $results->map(function ($value) use ($column) {
791 return $this->model->newFromBuilder([$column => $value])->{$column};
792 });
793 }
794
795 /**
796 * Paginate the given query.
797 *
798 * @param int|null $perPage
799 * @param array $columns
800 * @param string $pageName
801 * @param int|null $page
802 * @return \FluentBoards\Framework\Pagination\LengthAwarePaginatorInterface
803 *
804 * @throws \InvalidArgumentException
805 */
806 public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
807 {
808 $page = $page ?: Paginator::resolveCurrentPage($pageName);
809
810 $perPage = $perPage ?: $this->model->getPerPage();
811
812 $results = ($total = $this->toBase()->getCountForPagination())
813 ? $this->forPage($page, $perPage)->get($columns)
814 : $this->model->newCollection();
815
816 return $this->paginator($results, $total, $perPage, $page, [
817 'path' => Paginator::resolveCurrentPath(),
818 'pageName' => $pageName,
819 ]);
820 }
821
822 /**
823 * Paginate the given query into a simple paginator.
824 *
825 * @param int|null $perPage
826 * @param array $columns
827 * @param string $pageName
828 * @param int|null $page
829 * @return \FluentBoards\Framework\Pagination\PaginatorInterface
830 */
831 public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
832 {
833 $page = $page ?: Paginator::resolveCurrentPage($pageName);
834
835 $perPage = $perPage ?: $this->model->getPerPage();
836
837 // Next we will set the limit and offset for this query so that when we get the
838 // results we get the proper section of results. Then, we'll create the full
839 // paginator instances for these results with the given page and per page.
840 $this->skip(($page - 1) * $perPage)->take($perPage + 1);
841
842 return $this->simplePaginator($this->get($columns), $perPage, $page, [
843 'path' => Paginator::resolveCurrentPath(),
844 'pageName' => $pageName,
845 ]);
846 }
847
848 /**
849 * Paginate the given query into a cursor paginator.
850 *
851 * @param int|null $perPage
852 * @param array $columns
853 * @param string $cursorName
854 * @param \FluentBoards\Framework\Pagination\Cursor|string|null $cursor
855 * @return \FluentBoards\Framework\Pagination\CursorPaginator
856 */
857 public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
858 {
859 $perPage = $perPage ?: $this->model->getPerPage();
860
861 return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor);
862 }
863
864 /**
865 * Ensure the proper order by required for cursor pagination.
866 *
867 * @param bool $shouldReverse
868 * @return \FluentBoards\Framework\Support\Collection
869 */
870 protected function ensureOrderForCursorPagination($shouldReverse = false)
871 {
872 if (empty($this->query->orders) && empty($this->query->unionOrders)) {
873 $this->enforceOrderBy();
874 }
875
876 if ($shouldReverse) {
877 $this->query->orders = Collection::make($this->query->orders)->map(function ($order) {
878 $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';
879
880 return $order;
881 })->toArray();
882 }
883
884 if ($this->query->unionOrders) {
885 return Collection::make($this->query->unionOrders);
886 }
887
888 return Collection::make($this->query->orders);
889 }
890
891 /**
892 * Save a new model and return the instance.
893 *
894 * @param array $attributes
895 * @return \FluentBoards\Framework\Database\Orm\Model|$this
896 */
897 public function create(array $attributes = [])
898 {
899 return Helper::tap($this->newModelInstance($attributes), function ($instance) {
900 $instance->save();
901 });
902 }
903
904 /**
905 * Save a new model and return the instance. Allow mass-assignment.
906 *
907 * @param array $attributes
908 * @return \FluentBoards\Framework\Database\Orm\Model|$this
909 */
910 public function forceCreate(array $attributes)
911 {
912 return $this->model->unguarded(function () use ($attributes) {
913 return $this->newModelInstance()->create($attributes);
914 });
915 }
916
917 /**
918 * Update records in the database.
919 *
920 * @param array $values
921 * @return int
922 */
923 public function update(array $values)
924 {
925 return $this->toBase()->update($this->addUpdatedAtColumn($values));
926 }
927
928 /**
929 * Insert new records or update the existing ones.
930 *
931 * @param array $values
932 * @param array|string $uniqueBy
933 * @param array|null $update
934 * @return int
935 */
936 public function upsert(array $values, $uniqueBy, $update = null)
937 {
938 if (empty($values)) {
939 return 0;
940 }
941
942 if (! is_array(reset($values))) {
943 $values = [$values];
944 }
945
946 if (is_null($update)) {
947 $update = array_keys(reset($values));
948 }
949
950 return $this->toBase()->upsert(
951 $this->addTimestampsToUpsertValues($values),
952 $uniqueBy,
953 $this->addUpdatedAtToUpsertColumns($update)
954 );
955 }
956
957 /**
958 * Increment a column's value by a given amount.
959 *
960 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
961 * @param float|int $amount
962 * @param array $extra
963 * @return int
964 */
965 public function increment($column, $amount = 1, array $extra = [])
966 {
967 return $this->toBase()->increment(
968 $column, $amount, $this->addUpdatedAtColumn($extra)
969 );
970 }
971
972 /**
973 * Decrement a column's value by a given amount.
974 *
975 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
976 * @param float|int $amount
977 * @param array $extra
978 * @return int
979 */
980 public function decrement($column, $amount = 1, array $extra = [])
981 {
982 return $this->toBase()->decrement(
983 $column, $amount, $this->addUpdatedAtColumn($extra)
984 );
985 }
986
987 /**
988 * Add the "updated at" column to an array of values.
989 *
990 * @param array $values
991 * @return array
992 */
993 protected function addUpdatedAtColumn(array $values)
994 {
995 if (!$this->model->usesTimestamps() ||
996 is_null($this->model->getUpdatedAtColumn())) {
997 return $values;
998 }
999
1000 $values[$this->model->getUpdatedAtColumn()] = $this->model->freshTimestampString();
1001 return $values;
1002 }
1003
1004 /**
1005 * Add timestamps to the inserted values.
1006 *
1007 * @param array $values
1008 * @return array
1009 */
1010 protected function addTimestampsToUpsertValues(array $values)
1011 {
1012 if (! $this->model->usesTimestamps()) {
1013 return $values;
1014 }
1015
1016 $timestamp = $this->model->freshTimestampString();
1017
1018 $columns = array_filter([
1019 $this->model->getCreatedAtColumn(),
1020 $this->model->getUpdatedAtColumn(),
1021 ]);
1022
1023 foreach ($columns as $column) {
1024 foreach ($values as &$row) {
1025 $row = array_merge([$column => $timestamp], $row);
1026 }
1027 }
1028
1029 return $values;
1030 }
1031
1032 /**
1033 * Add the "updated at" column to the updated columns.
1034 *
1035 * @param array $update
1036 * @return array
1037 */
1038 protected function addUpdatedAtToUpsertColumns(array $update)
1039 {
1040 if (! $this->model->usesTimestamps()) {
1041 return $update;
1042 }
1043
1044 $column = $this->model->getUpdatedAtColumn();
1045
1046 if (! is_null($column) &&
1047 ! array_key_exists($column, $update) &&
1048 ! in_array($column, $update)) {
1049 $update[] = $column;
1050 }
1051
1052 return $update;
1053 }
1054
1055 /**
1056 * Delete records from the database.
1057 *
1058 * @return mixed
1059 */
1060 public function delete()
1061 {
1062 if (isset($this->onDelete)) {
1063 return call_user_func($this->onDelete, $this);
1064 }
1065
1066 return $this->toBase()->delete();
1067 }
1068
1069 /**
1070 * Run the default delete function on the builder.
1071 *
1072 * Since we do not apply scopes here, the row will actually be deleted.
1073 *
1074 * @return mixed
1075 */
1076 public function forceDelete()
1077 {
1078 return $this->query->delete();
1079 }
1080
1081 /**
1082 * Register a replacement for the default delete function.
1083 *
1084 * @param \Closure $callback
1085 * @return void
1086 */
1087 public function onDelete(Closure $callback)
1088 {
1089 $this->onDelete = $callback;
1090 }
1091
1092 /**
1093 * Determine if the given model has a scope.
1094 *
1095 * @param string $scope
1096 * @return bool
1097 */
1098 public function hasNamedScope($scope)
1099 {
1100 return $this->model && $this->model->hasNamedScope($scope);
1101 }
1102
1103 /**
1104 * Call the given local model scopes.
1105 *
1106 * @param array|string $scopes
1107 * @return static|mixed
1108 */
1109 public function scopes($scopes)
1110 {
1111 $builder = $this;
1112
1113 foreach (Arr::wrap($scopes) as $scope => $parameters) {
1114 // If the scope key is an integer, then the scope was passed as the value and
1115 // the parameter list is empty, so we will format the scope name and these
1116 // parameters here. Then, we'll be ready to call the scope on the model.
1117 if (is_int($scope)) {
1118 [$scope, $parameters] = [$parameters, []];
1119 }
1120
1121 // Next we'll pass the scope callback to the callScope method which will take
1122 // care of grouping the "wheres" properly so the logical order doesn't get
1123 // messed up when adding scopes. Then we'll return back out the builder.
1124 $builder = $builder->callNamedScope(
1125 $scope, Arr::wrap($parameters)
1126 );
1127 }
1128
1129 return $builder;
1130 }
1131
1132 /**
1133 * Apply the scopes to the Orm builder instance and return it.
1134 *
1135 * @return static
1136 */
1137 public function applyScopes()
1138 {
1139 if (! $this->scopes) {
1140 return $this;
1141 }
1142
1143 $builder = clone $this;
1144
1145 foreach ($this->scopes as $identifier => $scope) {
1146 if (! isset($builder->scopes[$identifier])) {
1147 continue;
1148 }
1149
1150 $builder->callScope(function (self $builder) use ($scope) {
1151 // If the scope is a Closure we will just go ahead and call the scope with the
1152 // builder instance. The "callScope" method will properly group the clauses
1153 // that are added to this query so "where" clauses maintain proper logic.
1154 if ($scope instanceof Closure) {
1155 $scope($builder);
1156 }
1157
1158 // If the scope is a scope object, we will call the apply method on this scope
1159 // passing in the builder and the model instance. After we run all of these
1160 // scopes we will return back the builder instance to the outside caller.
1161 if ($scope instanceof Scope) {
1162 $scope->apply($builder, $this->getModel());
1163 }
1164 });
1165 }
1166
1167 return $builder;
1168 }
1169
1170 /**
1171 * Apply the given scope on the current builder instance.
1172 *
1173 * @param callable $scope
1174 * @param array $parameters
1175 * @return mixed
1176 */
1177 protected function callScope(callable $scope, array $parameters = [])
1178 {
1179 array_unshift($parameters, $this);
1180
1181 $query = $this->getQuery();
1182
1183 // We will keep track of how many wheres are on the query before running the
1184 // scope so that we can properly group the added scope constraints in the
1185 // query as their own isolated nested where statement and avoid issues.
1186 $originalWhereCount = is_null($query->wheres)
1187 ? 0 : count($query->wheres);
1188
1189 $result = $scope(...array_values($parameters)) ?? $this;
1190
1191 if (count((array) $query->wheres) > $originalWhereCount) {
1192 $this->addNewWheresWithinGroup($query, $originalWhereCount);
1193 }
1194
1195 return $result;
1196 }
1197
1198 /**
1199 * Apply the given named scope on the current builder instance.
1200 *
1201 * @param string $scope
1202 * @param array $parameters
1203 * @return mixed
1204 */
1205 protected function callNamedScope($scope, array $parameters = [])
1206 {
1207 return $this->callScope(function (...$parameters) use ($scope) {
1208 return $this->model->callNamedScope($scope, $parameters);
1209 }, $parameters);
1210 }
1211
1212 /**
1213 * Nest where conditions by slicing them at the given where count.
1214 *
1215 * @param \FluentBoards\Framework\Database\Query\Builder $query
1216 * @param int $originalWhereCount
1217 * @return void
1218 */
1219 protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
1220 {
1221 // Here, we totally remove all of the where clauses since we are going to
1222 // rebuild them as nested queries by slicing the groups of wheres into
1223 // their own sections. This is to prevent any confusing logic order.
1224 $allWheres = $query->wheres;
1225
1226 $query->wheres = [];
1227
1228 $this->groupWhereSliceForScope(
1229 $query, array_slice($allWheres, 0, $originalWhereCount)
1230 );
1231
1232 $this->groupWhereSliceForScope(
1233 $query, array_slice($allWheres, $originalWhereCount)
1234 );
1235 }
1236
1237 /**
1238 * Slice where conditions at the given offset and add them to the query as a nested condition.
1239 *
1240 * @param \FluentBoards\Framework\Database\Query\Builder $query
1241 * @param array $whereSlice
1242 * @return void
1243 */
1244 protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
1245 {
1246 $whereBooleans = Collection::make($whereSlice)->pluck('boolean');
1247
1248 // Here we'll check if the given subset of where clauses contains any "or"
1249 // booleans and in this case create a nested where expression. That way
1250 // we don't add any unnecessary nesting thus keeping the query clean.
1251 if ($whereBooleans->contains('or')) {
1252 $query->wheres[] = $this->createNestedWhere(
1253 $whereSlice, $whereBooleans->first()
1254 );
1255 } else {
1256 $query->wheres = array_merge($query->wheres, $whereSlice);
1257 }
1258 }
1259
1260 /**
1261 * Create a where array with nested where conditions.
1262 *
1263 * @param array $whereSlice
1264 * @param string $boolean
1265 * @return array
1266 */
1267 protected function createNestedWhere($whereSlice, $boolean = 'and')
1268 {
1269 $whereGroup = $this->getQuery()->forNestedWhere();
1270
1271 $whereGroup->wheres = $whereSlice;
1272
1273 return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
1274 }
1275
1276 /**
1277 * Set the relationships that should be eager loaded.
1278 *
1279 * @param string|array $relations
1280 * @param string|\Closure|null $callback
1281 * @return $this
1282 */
1283 public function with($relations, $callback = null)
1284 {
1285 if ($callback instanceof Closure) {
1286 $eagerLoad = $this->parseWithRelations([$relations => $callback]);
1287 } else {
1288 $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
1289 }
1290
1291 $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
1292
1293 return $this;
1294 }
1295
1296 /**
1297 * Prevent the specified relations from being eager loaded.
1298 *
1299 * @param mixed $relations
1300 * @return $this
1301 */
1302 public function without($relations)
1303 {
1304 $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip(
1305 is_string($relations) ? func_get_args() : $relations
1306 ));
1307
1308 return $this;
1309 }
1310
1311 /**
1312 * Set the relationships that should be eager loaded while removing any previously added eager loading specifications.
1313 *
1314 * @param mixed $relations
1315 * @return $this
1316 */
1317 public function withOnly($relations)
1318 {
1319 $this->eagerLoad = [];
1320
1321 return $this->with($relations);
1322 }
1323
1324 /**
1325 * Create a new instance of the model being queried.
1326 *
1327 * @param array $attributes
1328 * @return \FluentBoards\Framework\Database\Orm\Model|static
1329 */
1330 public function newModelInstance($attributes = [])
1331 {
1332 return $this->model->newInstance($attributes)->setConnection(
1333 $this->query->getConnection()->getName()
1334 );
1335 }
1336
1337 /**
1338 * Parse a list of relations into individuals.
1339 *
1340 * @param array $relations
1341 * @return array
1342 */
1343 protected function parseWithRelations(array $relations)
1344 {
1345 $results = [];
1346
1347 foreach ($relations as $name => $constraints) {
1348 // If the "name" value is a numeric key, we can assume that no constraints
1349 // have been specified. We will just put an empty Closure there so that
1350 // we can treat these all the same while we are looping through them.
1351 if (is_numeric($name)) {
1352 $name = $constraints;
1353
1354 [$name, $constraints] = Str::contains($name, ':')
1355 ? $this->createSelectWithConstraint($name)
1356 : [$name, static function () {
1357 //
1358 }];
1359 }
1360
1361 // We need to separate out any nested includes, which allows the developers
1362 // to load deep relationships using "dots" without stating each level of
1363 // the relationship with its own key in the array of eager-load names.
1364 $results = $this->addNestedWiths($name, $results);
1365
1366 $results[$name] = $constraints;
1367 }
1368
1369 return $results;
1370 }
1371
1372 /**
1373 * Create a constraint to select the given columns for the relation.
1374 *
1375 * @param string $name
1376 * @return array
1377 */
1378 protected function createSelectWithConstraint($name)
1379 {
1380 return [explode(':', $name)[0], static function ($query) use ($name) {
1381 $query->select(array_map(static function ($column) use ($query) {
1382 if (Str::contains($column, '.')) {
1383 return $column;
1384 }
1385
1386 return $query instanceof BelongsToMany
1387 ? $query->getRelated()->getTable().'.'.$column
1388 : $column;
1389 }, explode(',', explode(':', $name)[1])));
1390 }];
1391 }
1392
1393 /**
1394 * Parse the nested relationships in a relation.
1395 *
1396 * @param string $name
1397 * @param array $results
1398 * @return array
1399 */
1400 protected function addNestedWiths($name, $results)
1401 {
1402 $progress = [];
1403
1404 // If the relation has already been set on the result array, we will not set it
1405 // again, since that would override any constraints that were already placed
1406 // on the relationships. We will only set the ones that are not specified.
1407 foreach (explode('.', $name) as $segment) {
1408 $progress[] = $segment;
1409
1410 if (! isset($results[$last = implode('.', $progress)])) {
1411 $results[$last] = static function () {
1412 //
1413 };
1414 }
1415 }
1416
1417 return $results;
1418 }
1419
1420 /**
1421 * Apply query-time casts to the model instance.
1422 *
1423 * @param array $casts
1424 * @return $this
1425 */
1426 public function withCasts($casts)
1427 {
1428 $this->model->mergeCasts($casts);
1429
1430 return $this;
1431 }
1432
1433 /**
1434 * Get the underlying query builder instance.
1435 *
1436 * @return \FluentBoards\Framework\Database\Query\Builder
1437 */
1438 public function getQuery()
1439 {
1440 return $this->query;
1441 }
1442
1443 /**
1444 * Set the underlying query builder instance.
1445 *
1446 * @param \FluentBoards\Framework\Database\Query\Builder $query
1447 * @return $this
1448 */
1449 public function setQuery($query)
1450 {
1451 $this->query = $query;
1452
1453 return $this;
1454 }
1455
1456 /**
1457 * Get a base query builder instance.
1458 *
1459 * @return \FluentBoards\Framework\Database\Query\Builder
1460 */
1461 public function toBase()
1462 {
1463 return $this->applyScopes()->getQuery();
1464 }
1465
1466 /**
1467 * Get the relationships being eagerly loaded.
1468 *
1469 * @return array
1470 */
1471 public function getEagerLoads()
1472 {
1473 return $this->eagerLoad;
1474 }
1475
1476 /**
1477 * Set the relationships being eagerly loaded.
1478 *
1479 * @param array $eagerLoad
1480 * @return $this
1481 */
1482 public function setEagerLoads(array $eagerLoad)
1483 {
1484 $this->eagerLoad = $eagerLoad;
1485
1486 return $this;
1487 }
1488
1489 /**
1490 * Get the default key name of the table.
1491 *
1492 * @return string
1493 */
1494 protected function defaultKeyName()
1495 {
1496 return $this->getModel()->getKeyName();
1497 }
1498
1499 /**
1500 * Get the model instance being queried.
1501 *
1502 * @return \FluentBoards\Framework\Database\Orm\Model|static
1503 */
1504 public function getModel()
1505 {
1506 return $this->model;
1507 }
1508
1509 /**
1510 * Set a model instance for the model being queried.
1511 *
1512 * @param \FluentBoards\Framework\Database\Orm\Model $model
1513 * @return $this
1514 */
1515 public function setModel(Model $model)
1516 {
1517 $this->model = $model;
1518
1519 $this->query->from($model->getTable());
1520
1521 return $this;
1522 }
1523
1524 /**
1525 * Qualify the given column name by the model's table.
1526 *
1527 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1528 * @return string
1529 */
1530 public function qualifyColumn($column)
1531 {
1532 return $this->model->qualifyColumn($column);
1533 }
1534
1535 /**
1536 * Qualify the given columns with the model's table.
1537 *
1538 * @param array|\FluentBoards\Framework\Database\Query\Expression $columns
1539 * @return array
1540 */
1541 public function qualifyColumns($columns)
1542 {
1543 return $this->model->qualifyColumns($columns);
1544 }
1545
1546 /**
1547 * Get the given macro by name.
1548 *
1549 * @param string $name
1550 * @return \Closure
1551 */
1552 public function getMacro($name)
1553 {
1554 return Arr::get($this->localMacros, $name);
1555 }
1556
1557 /**
1558 * Checks if a macro is registered.
1559 *
1560 * @param string $name
1561 * @return bool
1562 */
1563 public function hasMacro($name)
1564 {
1565 return isset($this->localMacros[$name]);
1566 }
1567
1568 /**
1569 * Get the given global macro by name.
1570 *
1571 * @param string $name
1572 * @return \Closure
1573 */
1574 public static function getGlobalMacro($name)
1575 {
1576 return Arr::get(static::$macros, $name);
1577 }
1578
1579 /**
1580 * Checks if a global macro is registered.
1581 *
1582 * @param string $name
1583 * @return bool
1584 */
1585 public static function hasGlobalMacro($name)
1586 {
1587 return isset(static::$macros[$name]);
1588 }
1589
1590 /**
1591 * Dynamically access builder proxies.
1592 *
1593 * @param string $key
1594 * @return mixed
1595 *
1596 * @throws \Exception
1597 */
1598 public function __get($key)
1599 {
1600 if ($key === 'orWhere') {
1601 return new HigherOrderBuilderProxy($this, $key);
1602 }
1603
1604 if (in_array($key, $this->propertyPassthru)) {
1605 return $this->toBase()->{$key};
1606 }
1607
1608 throw new Exception("Property [{$key}] does not exist on the Orm builder instance.");
1609 }
1610
1611 /**
1612 * Dynamically handle calls into the query instance.
1613 *
1614 * @param string $method
1615 * @param array $parameters
1616 * @return mixed
1617 */
1618 public function __call($method, $parameters)
1619 {
1620 if ($method === 'macro') {
1621 $this->localMacros[$parameters[0]] = $parameters[1];
1622
1623 return;
1624 }
1625
1626 if ($this->hasMacro($method)) {
1627 array_unshift($parameters, $this);
1628
1629 return $this->localMacros[$method](...$parameters);
1630 }
1631
1632 if (static::hasGlobalMacro($method)) {
1633 $callable = static::$macros[$method];
1634
1635 if ($callable instanceof Closure) {
1636 $callable = $callable->bindTo($this, static::class);
1637 }
1638
1639 return $callable(...$parameters);
1640 }
1641
1642 if ($this->hasNamedScope($method)) {
1643 return $this->callNamedScope($method, $parameters);
1644 }
1645
1646 if (in_array($method, $this->passthru)) {
1647 return $this->toBase()->{$method}(...$parameters);
1648 }
1649
1650 $this->forwardCallTo($this->query, $method, $parameters);
1651
1652 return $this;
1653 }
1654
1655 /**
1656 * Dynamically handle calls into the query instance.
1657 *
1658 * @param string $method
1659 * @param array $parameters
1660 * @return mixed
1661 *
1662 * @throws \BadMethodCallException
1663 */
1664 public static function __callStatic($method, $parameters)
1665 {
1666 if ($method === 'macro') {
1667 static::$macros[$parameters[0]] = $parameters[1];
1668
1669 return;
1670 }
1671
1672 if ($method === 'mixin') {
1673 return static::registerMixin($parameters[0], $parameters[1] ?? true);
1674 }
1675
1676 if (! static::hasGlobalMacro($method)) {
1677 static::throwBadMethodCallException($method);
1678 }
1679
1680 $callable = static::$macros[$method];
1681
1682 if ($callable instanceof Closure) {
1683 $callable = $callable->bindTo(null, static::class);
1684 }
1685
1686 return $callable(...$parameters);
1687 }
1688
1689 /**
1690 * Register the given mixin with the builder.
1691 *
1692 * @param string $mixin
1693 * @param bool $replace
1694 * @return void
1695 */
1696 protected static function registerMixin($mixin, $replace)
1697 {
1698 $methods = (new ReflectionClass($mixin))->getMethods(
1699 ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
1700 );
1701
1702 foreach ($methods as $method) {
1703 if ($replace || ! static::hasGlobalMacro($method->name)) {
1704 $method->setAccessible(true);
1705
1706 static::macro($method->name, $method->invoke($mixin));
1707 }
1708 }
1709 }
1710
1711 /**
1712 * Clone the Orm query builder.
1713 *
1714 * @return static
1715 */
1716 public function clone()
1717 {
1718 return clone $this;
1719 }
1720
1721 /**
1722 * Force a clone of the underlying query builder when cloning.
1723 *
1724 * @return void
1725 */
1726 public function __clone()
1727 {
1728 $this->query = clone $this->query;
1729 }
1730 }
1731