$modelClass * @param TBuilder $query * @param (\Closure(TBuilder):(void|TBuilder))|null $searchQuery */ public function searchQuery( string $modelClass, Builder $query, string $search, ?\Closure $searchQuery = null, string ...$fields ): Builder { if (empty($fields)) { return $query; } $words = array_filter(explode(' ', $search)); $query->where(function ($q) use ($modelClass, $searchQuery, $words, $search, $fields): void { // * Searching words foreach ($words as $word) { $this->searchOnFields($modelClass, $q, $word, ...$fields); } // * Searching whole sentence $this->searchOnFields($modelClass, $q, $search, ...$fields); if ($searchQuery) { $searchQuery($q); } }); return $query; } /** @var array> */ private static array $columnsCache = []; public static function clearColumnsCache(): void { self::$columnsCache = []; } private function hasTableColumn(string $table, string $column): bool { if (! isset(self::$columnsCache[$table])) { self::$columnsCache[$table] = Schema::getColumnListing($table); } return in_array($column, self::$columnsCache[$table], true); } /** * Sort columns with a query * * @template TModel of \Illuminate\Database\Eloquent\Model * @template TBuilder of \Illuminate\Contracts\Database\Query\Builder * * @param class-string $modelClass * @param TBuilder $query * @param array $ignore Columns to ignore for sorting. * @param array $columnMap Map virtual column names to database columns. */ public function sortQuery( string $modelClass, Builder $query, array $ignore = [], ?string $defaultSortCol = null, SortDirection $defaultSortWay = SortDirection::Ascending, array $columnMap = [], ): Builder { $table = (new $modelClass())->getTable(); $rName = $this->request->route()?->getName() ?? 'noroute'; if ($this->request->has(self::RESET_SORT_KEY)) { $this->resetSortSession($rName); } $sort = $this->getSortRequest(); $sort_col = $sort['sort_col']; $sort_way = $sort['sort_way']; if ((! $sort_col || $sort_col === 'undefined') && $defaultSortCol !== null) { $sort_col = $defaultSortCol; $sort_way = $defaultSortWay; } $this->persistSortSession($rName, $sort_col, $sort_way); $dbCol = $columnMap[$sort_col] ?? $sort_col; if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $dbCol)) { $direction = match ($sort_way) { SortDirection::Descending => 'desc', default => 'asc', }; $query->orderBy("{$table}.{$dbCol}", $direction); } elseif ($this->hasTableColumn($table, self::ORDER_COLUMN)) { $query->orderBy(self::ORDER_COLUMN, 'asc'); $this->persistSortSession( $rName, self::ORDER_COLUMN, SortDirection::Ascending ); } return $query; } /** * @template TModel of \Illuminate\Database\Eloquent\Model * * @param class-string $modelClass */ private function searchOnFields( string $modelClass, Builder $query, string $search, string ...$fields ): Builder { $table = (new $modelClass())->getTable(); $searchTerm = "%{$search}%"; foreach ($fields as $field) { $field = str_replace('?', '\?', $field); $searchValue = $this->getSearchValue( $modelClass, $search, $field ); if (is_array($searchValue)) { foreach ($searchValue as $enumItem) { $query->orWhereLike( "{$table}.{$field}", "%{$enumItem->value}%" ); } continue; } $query->orWhereLike("{$table}.{$field}", $searchTerm); } return $query; } /** * @template TModel of \Illuminate\Database\Eloquent\Model * * @param class-string $modelClass * @return string|array */ private function getSearchValue( string $modelClass, string $search, string $field ) { $cast = (new $modelClass())->getCasts()[$field] ?? null; if ($cast && class_exists($cast) && is_subclass_of($cast, BaseEnum::class)) { $searchLower = mb_strtolower($search); return array_values(array_filter( $cast::cases(), fn ($item): bool => str_contains( // @phpstan-ignore method.notFound (PhpStan cannot detect the label() method on BaseEnum cases) mb_strtolower((string) $item->label()), $searchLower ) )); } return $search; } /** @return array{sort_col:string,sort_way:\SortDirection} */ public function getSortRequest(): array { $rName = $this->request->route()?->getName() ?? 'noroute'; return [ 'sort_col' => $this->request->string( self::CACHE_SORT_COL_KEY, $this->session->get("{$rName}." . self::CACHE_SORT_COL_KEY, 'undefined') )->value(), 'sort_way' => $this->request->input( self::CACHE_SORT_WAY_KEY, $this->session->get("{$rName}." . self::CACHE_SORT_WAY_KEY) ) === 'desc' ? SortDirection::Descending : SortDirection::Ascending, ]; } public function persistSortSession(string $rName, ?string $sortCol, ?SortDirection $sortWay): void { if ($this->request->has(self::CACHE_SORT_COL_KEY)) { $this->session->put("{$rName}." . self::CACHE_SORT_COL_KEY, $sortCol); } if ($this->request->has(self::CACHE_SORT_WAY_KEY)) { $direction = match ($sortWay) { SortDirection::Descending => 'desc', default => 'asc', }; $this->session->put("{$rName}." . self::CACHE_SORT_WAY_KEY, $direction); } } public function resetSortSession(string $rName): void { $this->session->forget([ "{$rName}." . self::CACHE_SORT_COL_KEY, "{$rName}." . self::CACHE_SORT_WAY_KEY, "{$rName}." . self::CACHE_SORTED_KEY, ]); } /** * Helper to easily paginate and keep query parameters. * * @template TModel of \Illuminate\Database\Eloquent\Model * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Pagination\LengthAwarePaginator */ public function paginateQuery(\Illuminate\Database\Eloquent\Builder $query, Pagination $pagination): LengthAwarePaginator { return $query->paginate( perPage: $pagination->pageSize()->value(), page: $pagination->pageNumber()->value() )->withQueryString(); } }