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

1,759 lines 48.7 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 $column = $this->model->getUpdatedAtColumn();
1001
1002 $values = array_merge(
1003 [$column => $this->model->freshTimestampString()],
1004 $values
1005 );
1006
1007 $segments = preg_split('/\s+as\s+/i', $this->query->from);
1008
1009 $tableAlias = end($segments);
1010
1011 $connectionName = $this->model->getConnection()->getName();
1012
1013 if ($connectionName === 'sqlite') {
1014 if (count($segments) > 1) {
1015 $qualifiedColumn = $tableAlias . '.' . $column;
1016 } else {
1017 $qualifiedColumn = $column;
1018 }
1019 } else {
1020 $qualifiedColumn = $tableAlias . '.' . $column;
1021 }
1022
1023 $values[$qualifiedColumn] = Arr::get(
1024 $values, $qualifiedColumn, $values[$column]
1025 );
1026
1027 unset($values[$column]);
1028
1029 return $values;
1030 }
1031
1032 /**
1033 * Add timestamps to the inserted values.
1034 *
1035 * @param array $values
1036 * @return array
1037 */
1038 protected function addTimestampsToUpsertValues(array $values)
1039 {
1040 if (! $this->model->usesTimestamps()) {
1041 return $values;
1042 }
1043
1044 $timestamp = $this->model->freshTimestampString();
1045
1046 $columns = array_filter([
1047 $this->model->getCreatedAtColumn(),
1048 $this->model->getUpdatedAtColumn(),
1049 ]);
1050
1051 foreach ($columns as $column) {
1052 foreach ($values as &$row) {
1053 $row = array_merge([$column => $timestamp], $row);
1054 }
1055 }
1056
1057 return $values;
1058 }
1059
1060 /**
1061 * Add the "updated at" column to the updated columns.
1062 *
1063 * @param array $update
1064 * @return array
1065 */
1066 protected function addUpdatedAtToUpsertColumns(array $update)
1067 {
1068 if (! $this->model->usesTimestamps()) {
1069 return $update;
1070 }
1071
1072 $column = $this->model->getUpdatedAtColumn();
1073
1074 if (! is_null($column) &&
1075 ! array_key_exists($column, $update) &&
1076 ! in_array($column, $update)) {
1077 $update[] = $column;
1078 }
1079
1080 return $update;
1081 }
1082
1083 /**
1084 * Delete records from the database.
1085 *
1086 * @return mixed
1087 */
1088 public function delete()
1089 {
1090 if (isset($this->onDelete)) {
1091 return call_user_func($this->onDelete, $this);
1092 }
1093
1094 return $this->toBase()->delete();
1095 }
1096
1097 /**
1098 * Run the default delete function on the builder.
1099 *
1100 * Since we do not apply scopes here, the row will actually be deleted.
1101 *
1102 * @return mixed
1103 */
1104 public function forceDelete()
1105 {
1106 return $this->query->delete();
1107 }
1108
1109 /**
1110 * Register a replacement for the default delete function.
1111 *
1112 * @param \Closure $callback
1113 * @return void
1114 */
1115 public function onDelete(Closure $callback)
1116 {
1117 $this->onDelete = $callback;
1118 }
1119
1120 /**
1121 * Determine if the given model has a scope.
1122 *
1123 * @param string $scope
1124 * @return bool
1125 */
1126 public function hasNamedScope($scope)
1127 {
1128 return $this->model && $this->model->hasNamedScope($scope);
1129 }
1130
1131 /**
1132 * Call the given local model scopes.
1133 *
1134 * @param array|string $scopes
1135 * @return static|mixed
1136 */
1137 public function scopes($scopes)
1138 {
1139 $builder = $this;
1140
1141 foreach (Arr::wrap($scopes) as $scope => $parameters) {
1142 // If the scope key is an integer, then the scope was passed as the value and
1143 // the parameter list is empty, so we will format the scope name and these
1144 // parameters here. Then, we'll be ready to call the scope on the model.
1145 if (is_int($scope)) {
1146 [$scope, $parameters] = [$parameters, []];
1147 }
1148
1149 // Next we'll pass the scope callback to the callScope method which will take
1150 // care of grouping the "wheres" properly so the logical order doesn't get
1151 // messed up when adding scopes. Then we'll return back out the builder.
1152 $builder = $builder->callNamedScope(
1153 $scope, Arr::wrap($parameters)
1154 );
1155 }
1156
1157 return $builder;
1158 }
1159
1160 /**
1161 * Apply the scopes to the Orm builder instance and return it.
1162 *
1163 * @return static
1164 */
1165 public function applyScopes()
1166 {
1167 if (! $this->scopes) {
1168 return $this;
1169 }
1170
1171 $builder = clone $this;
1172
1173 foreach ($this->scopes as $identifier => $scope) {
1174 if (! isset($builder->scopes[$identifier])) {
1175 continue;
1176 }
1177
1178 $builder->callScope(function (self $builder) use ($scope) {
1179 // If the scope is a Closure we will just go ahead and call the scope with the
1180 // builder instance. The "callScope" method will properly group the clauses
1181 // that are added to this query so "where" clauses maintain proper logic.
1182 if ($scope instanceof Closure) {
1183 $scope($builder);
1184 }
1185
1186 // If the scope is a scope object, we will call the apply method on this scope
1187 // passing in the builder and the model instance. After we run all of these
1188 // scopes we will return back the builder instance to the outside caller.
1189 if ($scope instanceof Scope) {
1190 $scope->apply($builder, $this->getModel());
1191 }
1192 });
1193 }
1194
1195 return $builder;
1196 }
1197
1198 /**
1199 * Apply the given scope on the current builder instance.
1200 *
1201 * @param callable $scope
1202 * @param array $parameters
1203 * @return mixed
1204 */
1205 protected function callScope(callable $scope, array $parameters = [])
1206 {
1207 array_unshift($parameters, $this);
1208
1209 $query = $this->getQuery();
1210
1211 // We will keep track of how many wheres are on the query before running the
1212 // scope so that we can properly group the added scope constraints in the
1213 // query as their own isolated nested where statement and avoid issues.
1214 $originalWhereCount = is_null($query->wheres)
1215 ? 0 : count($query->wheres);
1216
1217 $result = $scope(...array_values($parameters)) ?? $this;
1218
1219 if (count((array) $query->wheres) > $originalWhereCount) {
1220 $this->addNewWheresWithinGroup($query, $originalWhereCount);
1221 }
1222
1223 return $result;
1224 }
1225
1226 /**
1227 * Apply the given named scope on the current builder instance.
1228 *
1229 * @param string $scope
1230 * @param array $parameters
1231 * @return mixed
1232 */
1233 protected function callNamedScope($scope, array $parameters = [])
1234 {
1235 return $this->callScope(function (...$parameters) use ($scope) {
1236 return $this->model->callNamedScope($scope, $parameters);
1237 }, $parameters);
1238 }
1239
1240 /**
1241 * Nest where conditions by slicing them at the given where count.
1242 *
1243 * @param \FluentBoards\Framework\Database\Query\Builder $query
1244 * @param int $originalWhereCount
1245 * @return void
1246 */
1247 protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount)
1248 {
1249 // Here, we totally remove all of the where clauses since we are going to
1250 // rebuild them as nested queries by slicing the groups of wheres into
1251 // their own sections. This is to prevent any confusing logic order.
1252 $allWheres = $query->wheres;
1253
1254 $query->wheres = [];
1255
1256 $this->groupWhereSliceForScope(
1257 $query, array_slice($allWheres, 0, $originalWhereCount)
1258 );
1259
1260 $this->groupWhereSliceForScope(
1261 $query, array_slice($allWheres, $originalWhereCount)
1262 );
1263 }
1264
1265 /**
1266 * Slice where conditions at the given offset and add them to the query as a nested condition.
1267 *
1268 * @param \FluentBoards\Framework\Database\Query\Builder $query
1269 * @param array $whereSlice
1270 * @return void
1271 */
1272 protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice)
1273 {
1274 $whereBooleans = Collection::make($whereSlice)->pluck('boolean');
1275
1276 // Here we'll check if the given subset of where clauses contains any "or"
1277 // booleans and in this case create a nested where expression. That way
1278 // we don't add any unnecessary nesting thus keeping the query clean.
1279 if ($whereBooleans->contains('or')) {
1280 $query->wheres[] = $this->createNestedWhere(
1281 $whereSlice, $whereBooleans->first()
1282 );
1283 } else {
1284 $query->wheres = array_merge($query->wheres, $whereSlice);
1285 }
1286 }
1287
1288 /**
1289 * Create a where array with nested where conditions.
1290 *
1291 * @param array $whereSlice
1292 * @param string $boolean
1293 * @return array
1294 */
1295 protected function createNestedWhere($whereSlice, $boolean = 'and')
1296 {
1297 $whereGroup = $this->getQuery()->forNestedWhere();
1298
1299 $whereGroup->wheres = $whereSlice;
1300
1301 return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean];
1302 }
1303
1304 /**
1305 * Set the relationships that should be eager loaded.
1306 *
1307 * @param string|array $relations
1308 * @param string|\Closure|null $callback
1309 * @return $this
1310 */
1311 public function with($relations, $callback = null)
1312 {
1313 if ($callback instanceof Closure) {
1314 $eagerLoad = $this->parseWithRelations([$relations => $callback]);
1315 } else {
1316 $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
1317 }
1318
1319 $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
1320
1321 return $this;
1322 }
1323
1324 /**
1325 * Prevent the specified relations from being eager loaded.
1326 *
1327 * @param mixed $relations
1328 * @return $this
1329 */
1330 public function without($relations)
1331 {
1332 $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip(
1333 is_string($relations) ? func_get_args() : $relations
1334 ));
1335
1336 return $this;
1337 }
1338
1339 /**
1340 * Set the relationships that should be eager loaded while removing any previously added eager loading specifications.
1341 *
1342 * @param mixed $relations
1343 * @return $this
1344 */
1345 public function withOnly($relations)
1346 {
1347 $this->eagerLoad = [];
1348
1349 return $this->with($relations);
1350 }
1351
1352 /**
1353 * Create a new instance of the model being queried.
1354 *
1355 * @param array $attributes
1356 * @return \FluentBoards\Framework\Database\Orm\Model|static
1357 */
1358 public function newModelInstance($attributes = [])
1359 {
1360 return $this->model->newInstance($attributes)->setConnection(
1361 $this->query->getConnection()->getName()
1362 );
1363 }
1364
1365 /**
1366 * Parse a list of relations into individuals.
1367 *
1368 * @param array $relations
1369 * @return array
1370 */
1371 protected function parseWithRelations(array $relations)
1372 {
1373 $results = [];
1374
1375 foreach ($relations as $name => $constraints) {
1376 // If the "name" value is a numeric key, we can assume that no constraints
1377 // have been specified. We will just put an empty Closure there so that
1378 // we can treat these all the same while we are looping through them.
1379 if (is_numeric($name)) {
1380 $name = $constraints;
1381
1382 [$name, $constraints] = Str::contains($name, ':')
1383 ? $this->createSelectWithConstraint($name)
1384 : [$name, static function () {
1385 //
1386 }];
1387 }
1388
1389 // We need to separate out any nested includes, which allows the developers
1390 // to load deep relationships using "dots" without stating each level of
1391 // the relationship with its own key in the array of eager-load names.
1392 $results = $this->addNestedWiths($name, $results);
1393
1394 $results[$name] = $constraints;
1395 }
1396
1397 return $results;
1398 }
1399
1400 /**
1401 * Create a constraint to select the given columns for the relation.
1402 *
1403 * @param string $name
1404 * @return array
1405 */
1406 protected function createSelectWithConstraint($name)
1407 {
1408 return [explode(':', $name)[0], static function ($query) use ($name) {
1409 $query->select(array_map(static function ($column) use ($query) {
1410 if (Str::contains($column, '.')) {
1411 return $column;
1412 }
1413
1414 return $query instanceof BelongsToMany
1415 ? $query->getRelated()->getTable().'.'.$column
1416 : $column;
1417 }, explode(',', explode(':', $name)[1])));
1418 }];
1419 }
1420
1421 /**
1422 * Parse the nested relationships in a relation.
1423 *
1424 * @param string $name
1425 * @param array $results
1426 * @return array
1427 */
1428 protected function addNestedWiths($name, $results)
1429 {
1430 $progress = [];
1431
1432 // If the relation has already been set on the result array, we will not set it
1433 // again, since that would override any constraints that were already placed
1434 // on the relationships. We will only set the ones that are not specified.
1435 foreach (explode('.', $name) as $segment) {
1436 $progress[] = $segment;
1437
1438 if (! isset($results[$last = implode('.', $progress)])) {
1439 $results[$last] = static function () {
1440 //
1441 };
1442 }
1443 }
1444
1445 return $results;
1446 }
1447
1448 /**
1449 * Apply query-time casts to the model instance.
1450 *
1451 * @param array $casts
1452 * @return $this
1453 */
1454 public function withCasts($casts)
1455 {
1456 $this->model->mergeCasts($casts);
1457
1458 return $this;
1459 }
1460
1461 /**
1462 * Get the underlying query builder instance.
1463 *
1464 * @return \FluentBoards\Framework\Database\Query\Builder
1465 */
1466 public function getQuery()
1467 {
1468 return $this->query;
1469 }
1470
1471 /**
1472 * Set the underlying query builder instance.
1473 *
1474 * @param \FluentBoards\Framework\Database\Query\Builder $query
1475 * @return $this
1476 */
1477 public function setQuery($query)
1478 {
1479 $this->query = $query;
1480
1481 return $this;
1482 }
1483
1484 /**
1485 * Get a base query builder instance.
1486 *
1487 * @return \FluentBoards\Framework\Database\Query\Builder
1488 */
1489 public function toBase()
1490 {
1491 return $this->applyScopes()->getQuery();
1492 }
1493
1494 /**
1495 * Get the relationships being eagerly loaded.
1496 *
1497 * @return array
1498 */
1499 public function getEagerLoads()
1500 {
1501 return $this->eagerLoad;
1502 }
1503
1504 /**
1505 * Set the relationships being eagerly loaded.
1506 *
1507 * @param array $eagerLoad
1508 * @return $this
1509 */
1510 public function setEagerLoads(array $eagerLoad)
1511 {
1512 $this->eagerLoad = $eagerLoad;
1513
1514 return $this;
1515 }
1516
1517 /**
1518 * Get the default key name of the table.
1519 *
1520 * @return string
1521 */
1522 protected function defaultKeyName()
1523 {
1524 return $this->getModel()->getKeyName();
1525 }
1526
1527 /**
1528 * Get the model instance being queried.
1529 *
1530 * @return \FluentBoards\Framework\Database\Orm\Model|static
1531 */
1532 public function getModel()
1533 {
1534 return $this->model;
1535 }
1536
1537 /**
1538 * Set a model instance for the model being queried.
1539 *
1540 * @param \FluentBoards\Framework\Database\Orm\Model $model
1541 * @return $this
1542 */
1543 public function setModel(Model $model)
1544 {
1545 $this->model = $model;
1546
1547 $this->query->from($model->getTable());
1548
1549 return $this;
1550 }
1551
1552 /**
1553 * Qualify the given column name by the model's table.
1554 *
1555 * @param string|\FluentBoards\Framework\Database\Query\Expression $column
1556 * @return string
1557 */
1558 public function qualifyColumn($column)
1559 {
1560 return $this->model->qualifyColumn($column);
1561 }
1562
1563 /**
1564 * Qualify the given columns with the model's table.
1565 *
1566 * @param array|\FluentBoards\Framework\Database\Query\Expression $columns
1567 * @return array
1568 */
1569 public function qualifyColumns($columns)
1570 {
1571 return $this->model->qualifyColumns($columns);
1572 }
1573
1574 /**
1575 * Get the given macro by name.
1576 *
1577 * @param string $name
1578 * @return \Closure
1579 */
1580 public function getMacro($name)
1581 {
1582 return Arr::get($this->localMacros, $name);
1583 }
1584
1585 /**
1586 * Checks if a macro is registered.
1587 *
1588 * @param string $name
1589 * @return bool
1590 */
1591 public function hasMacro($name)
1592 {
1593 return isset($this->localMacros[$name]);
1594 }
1595
1596 /**
1597 * Get the given global macro by name.
1598 *
1599 * @param string $name
1600 * @return \Closure
1601 */
1602 public static function getGlobalMacro($name)
1603 {
1604 return Arr::get(static::$macros, $name);
1605 }
1606
1607 /**
1608 * Checks if a global macro is registered.
1609 *
1610 * @param string $name
1611 * @return bool
1612 */
1613 public static function hasGlobalMacro($name)
1614 {
1615 return isset(static::$macros[$name]);
1616 }
1617
1618 /**
1619 * Dynamically access builder proxies.
1620 *
1621 * @param string $key
1622 * @return mixed
1623 *
1624 * @throws \Exception
1625 */
1626 public function __get($key)
1627 {
1628 if ($key === 'orWhere') {
1629 return new HigherOrderBuilderProxy($this, $key);
1630 }
1631
1632 if (in_array($key, $this->propertyPassthru)) {
1633 return $this->toBase()->{$key};
1634 }
1635
1636 throw new Exception("Property [{$key}] does not exist on the Orm builder instance.");
1637 }
1638
1639 /**
1640 * Dynamically handle calls into the query instance.
1641 *
1642 * @param string $method
1643 * @param array $parameters
1644 * @return mixed
1645 */
1646 public function __call($method, $parameters)
1647 {
1648 if ($method === 'macro') {
1649 $this->localMacros[$parameters[0]] = $parameters[1];
1650
1651 return;
1652 }
1653
1654 if ($this->hasMacro($method)) {
1655 array_unshift($parameters, $this);
1656
1657 return $this->localMacros[$method](...$parameters);
1658 }
1659
1660 if (static::hasGlobalMacro($method)) {
1661 $callable = static::$macros[$method];
1662
1663 if ($callable instanceof Closure) {
1664 $callable = $callable->bindTo($this, static::class);
1665 }
1666
1667 return $callable(...$parameters);
1668 }
1669
1670 if ($this->hasNamedScope($method)) {
1671 return $this->callNamedScope($method, $parameters);
1672 }
1673
1674 if (in_array($method, $this->passthru)) {
1675 return $this->toBase()->{$method}(...$parameters);
1676 }
1677
1678 $this->forwardCallTo($this->query, $method, $parameters);
1679
1680 return $this;
1681 }
1682
1683 /**
1684 * Dynamically handle calls into the query instance.
1685 *
1686 * @param string $method
1687 * @param array $parameters
1688 * @return mixed
1689 *
1690 * @throws \BadMethodCallException
1691 */
1692 public static function __callStatic($method, $parameters)
1693 {
1694 if ($method === 'macro') {
1695 static::$macros[$parameters[0]] = $parameters[1];
1696
1697 return;
1698 }
1699
1700 if ($method === 'mixin') {
1701 return static::registerMixin($parameters[0], $parameters[1] ?? true);
1702 }
1703
1704 if (! static::hasGlobalMacro($method)) {
1705 static::throwBadMethodCallException($method);
1706 }
1707
1708 $callable = static::$macros[$method];
1709
1710 if ($callable instanceof Closure) {
1711 $callable = $callable->bindTo(null, static::class);
1712 }
1713
1714 return $callable(...$parameters);
1715 }
1716
1717 /**
1718 * Register the given mixin with the builder.
1719 *
1720 * @param string $mixin
1721 * @param bool $replace
1722 * @return void
1723 */
1724 protected static function registerMixin($mixin, $replace)
1725 {
1726 $methods = (new ReflectionClass($mixin))->getMethods(
1727 ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
1728 );
1729
1730 foreach ($methods as $method) {
1731 if ($replace || ! static::hasGlobalMacro($method->name)) {
1732 $method->setAccessible(true);
1733
1734 static::macro($method->name, $method->invoke($mixin));
1735 }
1736 }
1737 }
1738
1739 /**
1740 * Clone the Orm query builder.
1741 *
1742 * @return static
1743 */
1744 public function clone()
1745 {
1746 return clone $this;
1747 }
1748
1749 /**
1750 * Force a clone of the underlying query builder when cloning.
1751 *
1752 * @return void
1753 */
1754 public function __clone()
1755 {
1756 $this->query = clone $this->query;
1757 }
1758 }
1759