*/ protected $resourceClass; /** * The original query builder instance. * * @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|null */ protected $originalQueryBuilder; /** * The query builder instance. * * @var \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|null */ protected $queryBuilder; /** * Optional callbacks before model query execution. * * @var array */ protected $queryCallbacks = []; /** * Determine query callbacks has been applied. * * @var bool */ protected $appliedQueryCallbacks = false; /** * Construct a new query builder for a resource. * * @param class-string<\Laravel\Nova\Resource> $resourceClass * @return void */ public function __construct($resourceClass) { $this->resourceClass = $resourceClass; } /** * Build a "whereKey" query for the given resource. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query * @param string $key * @return $this */ public function whereKey($query, $key) { $this->setOriginalQueryBuilder($this->queryBuilder = $query); $this->tap(function ($query) use ($key) { $query->whereKey($key); }); return $this; } /** * Build a "search" query for the given resource. * * @param \Laravel\Nova\Http\Requests\NovaRequest $request * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query * @param string|null $search * @param array $filters * @param array $orderings * @param string $withTrashed * @return $this */ public function search(NovaRequest $request, $query, $search = null, array $filters = [], array $orderings = [], $withTrashed = TrashedStatus::DEFAULT) { $this->setOriginalQueryBuilder($query); $hasSearchKeyword = ! empty(trim($search ?? '')); $hasOrderings = collect($orderings)->filter()->isNotEmpty(); if ($this->resourceClass::usesScout()) { if ($hasSearchKeyword) { $this->queryBuilder = $this->resourceClass::buildIndexQueryUsingScout($request, $search, $withTrashed); $search = ''; if ($query instanceof MorphToMany || $query instanceof BelongsToMany) { $this->tap(function ($queryBuilder) use ($query) { /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder */ $queryBuilder->whereIn( $this->resourceClass::newModel()->getQualifiedKeyName(), $query->allRelatedIds() ); }); } } if (! $hasSearchKeyword && ! $hasOrderings) { $this->tap(function ($query) { /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */ $this->resourceClass::defaultOrderings($query); }); } } if (! isset($this->queryBuilder)) { $this->queryBuilder = $query; } $this->tap(function ($query) use ($request, $search, $filters, $orderings, $withTrashed) { /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */ $this->resourceClass::buildIndexQuery( $request, $query, $search, $filters, $orderings, $withTrashed ); }); return $this; } /** * Pass the query to a given callback. * * @param callable(\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation):void $callback * @return $this */ public function tap($callback) { $this->queryCallbacks[] = $callback; return $this; } /** * Set the "take" directly to Scout or Eloquent builder. * * @param int $limit * @return $this */ public function take($limit) { $this->queryBuilder->take($limit); return $this; } /** * Defer setting a "limit" using query callback and only executed via Eloquent builder. * * @param int $limit * @return $this */ public function limit($limit) { return $this->tap(function ($query) use ($limit) { /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */ $query->limit($limit); }); } /** * Get the results of the search. * * @return \Illuminate\Database\Eloquent\Collection */ public function get() { return $this->applyQueryCallbacks($this->queryBuilder)->get(); } /** * Get a lazy collection for the given query by chunks of the given size. * * @param int $chunkSize * @return \Illuminate\Support\LazyCollection */ public function lazy($chunkSize = 1000) { if (! method_exists($this->queryBuilder, 'lazy')) { return $this->cursor(); } return $this->applyQueryCallbacks($this->queryBuilder)->lazy($chunkSize); } /** * Get a lazy collection for the given query. * * @return \Illuminate\Support\LazyCollection */ public function cursor() { $queryBuilder = $this->applyQueryCallbacks($this->queryBuilder); if ( method_exists($queryBuilder, 'cursor') && (! $queryBuilder instanceof ScoutBuilder && empty($queryBuilder->getEagerLoads())) ) { return $queryBuilder->cursor(); } return $queryBuilder->get()->lazy(); } /** * Get the paginated results of the query. * * @param int $perPage * @return array{0: \Illuminate\Contracts\Pagination\Paginator, 1: int|null, 2: bool} */ public function paginate($perPage) { $queryBuilder = $this->applyQueryCallbacks($this->queryBuilder); if (! $queryBuilder instanceof ScoutBuilder) { return [ $queryBuilder->simplePaginate($perPage), $this->getCountForPagination(), true, ]; } return $this->paginateFromScout($queryBuilder, $perPage); } /** * Get the paginated results of the Scout query. * * @param \Laravel\Scout\Builder $queryBuilder * @param int $perPage * @return array{0: \Illuminate\Contracts\Pagination\Paginator, 1: int|null, 2: false} */ protected function paginateFromScout(ScoutBuilder $queryBuilder, $perPage) { $originalQueryBuilder = clone $this->originalQueryBuilder; [$sql, $bindings] = [$originalQueryBuilder->toSql(), $originalQueryBuilder->getBindings()]; $modelQueryBuilder = $this->handleQueryCallbacks($originalQueryBuilder); if ($sql === $modelQueryBuilder->toSql() && array_diff($bindings, $modelQueryBuilder->getBindings()) === []) { /** @var \Illuminate\Pagination\LengthAwarePaginator $paginated */ $paginated = $queryBuilder->paginate($perPage); $items = $paginated->items(); $hasMorePages = ($paginated->perPage() * $paginated->currentPage()) < $paginated->total(); return [ app()->makeWith(Paginator::class, [ 'items' => $items, 'perPage' => $paginated->perPage(), 'currentPage' => $paginated->currentPage(), 'options' => $paginated->getOptions(), ])->hasMorePagesWhen($hasMorePages), $paginated->total(), false, ]; } /** @var array $scoutResultKeys */ $scoutResultKeys = $queryBuilder->keys()->all(); /** @var \Illuminate\Database\Eloquent\Model&\Laravel\Scout\Searchable $model */ $model = $this->resourceClass::newModel(); $paginated = tap($model->queryScoutModelsByIds( $queryBuilder, $scoutResultKeys ), function ($query) { /** @var \Illuminate\Database\Eloquent\Builder $query */ $this->originalQueryBuilder = $query; })->simplePaginate($perPage); if (! $model->searchableUsing() instanceof PaginatesEloquentModels) { /** @var array $objectIdPositions */ $objectIdPositions = collect($scoutResultKeys)->values()->flip()->all(); $paginated->setCollection( $paginated->getCollection() ->sortBy(function ($model) use ($objectIdPositions) { return $objectIdPositions[$model->getScoutKey()]; }, SORT_NUMERIC)->values() ); } return [$paginated, $this->getCountForPagination(), false]; } /** * Get the count of the total records for the paginator. * * @return int|null */ public function getCountForPagination() { return $this->toBaseQueryBuilder()->getCountForPagination(); } /** * Convert the query builder to an Eloquent query builder (skip using Scout). * * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation */ public function toBase() { return $this->applyQueryCallbacks($this->originalQueryBuilder); } /** * Convert the query builder to an fluent query builder (skip using Scout). * * @return \Illuminate\Database\Query\Builder */ public function toBaseQueryBuilder() { return $this->toBase()->toBase(); } /** * Set original query builder instance. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder * @return void */ protected function setOriginalQueryBuilder($queryBuilder) { if (isset($this->originalQueryBuilder)) { throw new RuntimeException('Unable to override $originalQueryBuilder, please create a new '.self::class); } $this->originalQueryBuilder = $queryBuilder; } /** * Apply any query callbacks to the query builder. * * @param \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder * @return \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation */ protected function applyQueryCallbacks($queryBuilder) { if (! $this->appliedQueryCallbacks) { $this->handleQueryCallbacks($queryBuilder); $this->appliedQueryCallbacks = true; } return $queryBuilder; } /** * Handle any query callbacks to the query builder. * * @param \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder * @return \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation */ protected function handleQueryCallbacks($queryBuilder) { $callback = function ($query) { /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */ collect($this->queryCallbacks) ->filter() ->each(function ($callback) use ($query) { call_user_func($callback, $query); }); }; if ($queryBuilder instanceof ScoutBuilder) { $queryBuilder->query($callback); } else { $queryBuilder->tap($callback); } return $queryBuilder; } }