<?php namespace Laravel\Scout\Engines; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use Illuminate\Support\LazyCollection; use Laravel\Scout\Attributes\SearchUsingFullText; use Laravel\Scout\Attributes\SearchUsingPrefix; use Laravel\Scout\Builder; use Laravel\Scout\Contracts\PaginatesEloquentModelsUsingDatabase; use ReflectionMethod; class DatabaseEngine extends Engine implements PaginatesEloquentModelsUsingDatabase { /** * Create a new engine instance. * * @return void */ public function __construct() { // } /** * Update the given model in the index. * * @param \Illuminate\Database\Eloquent\Collection $models * @return void */ public function update($models) { // } /** * Remove the given model from the index. * * @param \Illuminate\Database\Eloquent\Collection $models * @return void */ public function delete($models) { // } /** * Perform the given search on the engine. * * @param \Laravel\Scout\Builder $builder * @return mixed */ public function search(Builder $builder) { $models = $this->searchModels($builder); return [ 'results' => $models, 'total' => $models->count(), ]; } /** * Paginate the given search on the engine. * * @param \Laravel\Scout\Builder $builder * @param int $perPage * @param int $page * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function paginate(Builder $builder, $perPage, $page) { return $this->paginateUsingDatabase($builder, $perPage, 'page', $page); } /** * Paginate the given search on the engine. * * @param \Laravel\Scout\Builder $builder * @param int $perPage * @param string $pageName * @param int $page * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function paginateUsingDatabase(Builder $builder, $perPage, $pageName, $page) { return $this->buildSearchQuery($builder) ->when($builder->orders, function ($query) use ($builder) { foreach ($builder->orders as $order) { $query->orderBy($order['column'], $order['direction']); } }) ->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) { $query->orderBy($builder->model->getKeyName(), 'desc'); }) ->paginate($perPage, ['*'], $pageName, $page); } /** * Paginate the given search on the engine using simple pagination. * * @param \Laravel\Scout\Builder $builder * @param int $perPage * @param int $page * @return \Illuminate\Contracts\Pagination\Paginator */ public function simplePaginate(Builder $builder, $perPage, $page) { return $this->simplePaginateUsingDatabase($builder, $perPage, 'page', $page); } /** * Paginate the given query into a simple paginator. * * @param int $perPage * @param string $pageName * @param int|null $page * @return \Illuminate\Contracts\Pagination\Paginator */ public function simplePaginateUsingDatabase(Builder $builder, $perPage, $pageName, $page) { return $this->buildSearchQuery($builder) ->when($builder->orders, function ($query) use ($builder) { foreach ($builder->orders as $order) { $query->orderBy($order['column'], $order['direction']); } }) ->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) { $query->orderBy($builder->model->getKeyName(), 'desc'); }) ->simplePaginate($perPage, ['*'], $pageName, $page); } /** * Get the Eloquent models for the given builder. * * @param \Laravel\Scout\Builder $builder * @param int|null $page * @param int|null $perPage * @return \Illuminate\Database\Eloquent\Collection */ protected function searchModels(Builder $builder, $page = null, $perPage = null) { return $this->buildSearchQuery($builder) ->when(! is_null($page) && ! is_null($perPage), function ($query) use ($page, $perPage) { $query->forPage($page, $perPage); }) ->when($builder->orders, function ($query) use ($builder) { foreach ($builder->orders as $order) { $query->orderBy($order['column'], $order['direction']); } }) ->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) { $query->orderBy($builder->model->getKeyName(), 'desc'); }) ->get(); } /** * Initialize / build the search query for the given Scout builder. * * @param \Laravel\Scout\Builder $builder * @return \Illuminate\Database\Eloquent\Builder */ protected function buildSearchQuery(Builder $builder) { $query = $this->initializeSearchQuery( $builder, array_keys($builder->model->toSearchableArray()), $this->getPrefixColumns($builder), $this->getFullTextColumns($builder) ); return $this->constrainForSoftDeletes( $builder, $this->addAdditionalConstraints($builder, $query->take($builder->limit)) ); } /** * Build the initial text search database query for all relevant columns. * * @param \Laravel\Scout\Builder $builder * @param array $columns * @param array $prefixColumns * @param array $fullTextColumns * @return \Illuminate\Database\Eloquent\Builder */ protected function initializeSearchQuery(Builder $builder, array $columns, array $prefixColumns = [], array $fullTextColumns = []) { if (blank($builder->query)) { return $builder->model->newQuery(); } return $builder->model->newQuery()->where(function ($query) use ($builder, $columns, $prefixColumns, $fullTextColumns) { $connectionType = $builder->model->getConnection()->getDriverName(); $canSearchPrimaryKey = ctype_digit($builder->query) && in_array($builder->model->getKeyType(), ['int', 'integer']) && ($connectionType != 'pgsql' || $builder->query <= PHP_INT_MAX) && in_array($builder->model->getKeyName(), $columns); if ($canSearchPrimaryKey) { $query->orWhere($builder->model->getQualifiedKeyName(), $builder->query); } $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like'; foreach ($columns as $column) { if (in_array($column, $fullTextColumns)) { $query->orWhereFullText( $builder->model->qualifyColumn($column), $builder->query, $this->getFullTextOptions($builder) ); } else { if ($canSearchPrimaryKey && $column === $builder->model->getKeyName()) { continue; } $query->orWhere( $builder->model->qualifyColumn($column), $likeOperator, in_array($column, $prefixColumns) ? $builder->query.'%' : '%'.$builder->query.'%', ); } } }); } /** * Add additional, developer defined constraints to the search query. * * @param \Laravel\Scout\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function addAdditionalConstraints(Builder $builder, $query) { return $query->when(! is_null($builder->callback), function ($query) use ($builder) { call_user_func($builder->callback, $query, $builder, $builder->query); })->when(! $builder->callback && count($builder->wheres) > 0, function ($query) use ($builder) { foreach ($builder->wheres as $key => $value) { if ($key !== '__soft_deleted') { $query->where($key, '=', $value); } } })->when(! $builder->callback && count($builder->whereIns) > 0, function ($query) use ($builder) { foreach ($builder->whereIns as $key => $values) { $query->whereIn($key, $values); } })->when(! $builder->callback && count($builder->whereNotIns) > 0, function ($query) use ($builder) { foreach ($builder->whereNotIns as $key => $values) { $query->whereNotIn($key, $values); } })->when(! is_null($builder->queryCallback), function ($query) use ($builder) { call_user_func($builder->queryCallback, $query); }); } /** * Ensure that soft delete constraints are properly applied to the query. * * @param \Laravel\Scout\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function constrainForSoftDeletes($builder, $query) { if (Arr::get($builder->wheres, '__soft_deleted') === 0) { return $query->withoutTrashed(); } elseif (Arr::get($builder->wheres, '__soft_deleted') === 1) { return $query->onlyTrashed(); } elseif (in_array(SoftDeletes::class, class_uses_recursive(get_class($builder->model))) && config('scout.soft_delete', false)) { return $query->withTrashed(); } return $query; } /** * Get the full-text columns for the query. * * @param \Laravel\Scout\Builder $builder * @return array */ protected function getFullTextColumns(Builder $builder) { return $this->getAttributeColumns($builder, SearchUsingFullText::class); } /** * Get the prefix search columns for the query. * * @param \Laravel\Scout\Builder $builder * @return array */ protected function getPrefixColumns(Builder $builder) { return $this->getAttributeColumns($builder, SearchUsingPrefix::class); } /** * Get the columns marked with a given attribute. * * @param \Laravel\Scout\Builder $builder * @param string $attributeClass * @return array */ protected function getAttributeColumns(Builder $builder, $attributeClass) { $columns = []; if (PHP_MAJOR_VERSION < 8) { return []; } foreach ((new ReflectionMethod($builder->model, 'toSearchableArray'))->getAttributes() as $attribute) { if ($attribute->getName() !== $attributeClass) { continue; } $columns = array_merge($columns, Arr::wrap($attribute->getArguments()[0])); } return $columns; } /** * Get the full-text search options for the query. * * @param \Laravel\Scout\Builder $builder * @return array */ protected function getFullTextOptions(Builder $builder) { $options = []; if (PHP_MAJOR_VERSION < 8) { return []; } foreach ((new ReflectionMethod($builder->model, 'toSearchableArray'))->getAttributes(SearchUsingFullText::class) as $attribute) { $arguments = $attribute->getArguments()[1] ?? []; $options = array_merge($options, Arr::wrap($arguments)); } return $options; } /** * Pluck and return the primary keys of the given results. * * @param mixed $results * @return \Illuminate\Support\Collection */ public function mapIds($results) { $results = $results['results']; return count($results) > 0 ? collect($results->modelKeys()) : collect(); } /** * Map the given results to instances of the given model. * * @param \Laravel\Scout\Builder $builder * @param mixed $results * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Database\Eloquent\Collection */ public function map(Builder $builder, $results, $model) { return $results['results']; } /** * Map the given results to instances of the given model via a lazy collection. * * @param \Laravel\Scout\Builder $builder * @param mixed $results * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Support\LazyCollection */ public function lazyMap(Builder $builder, $results, $model) { return new LazyCollection($results['results']->all()); } /** * Get the total count from a raw result returned by the engine. * * @param mixed $results * @return int */ public function getTotalCount($results) { return $results['total']; } /** * Flush all of the model's records from the engine. * * @param \Illuminate\Database\Eloquent\Model $model * @return void */ public function flush($model) { // } /** * Create a search index. * * @param string $name * @param array $options * @return mixed * * @throws \Exception */ public function createIndex($name, array $options = []) { // } /** * Delete a search index. * * @param string $name * @return mixed */ public function deleteIndex($name) { // } }