query('q', '')); // Nothing to search for — send them to the all-products page rather than // render an empty results page. if ($query === '') { return redirect()->route('products'); } $sort = ProductSort::tryFrom((string) request()->query('sort')); $page = max(1, (int) request()->query('page', 1)); $cards = $this->cardsFor($query, $sort); $products = new LengthAwarePaginator( items: $cards->forPage($page, self::PER_PAGE)->values(), total: $cards->count(), perPage: self::PER_PAGE, currentPage: $page, options: ['path' => LengthAwarePaginator::resolveCurrentPath()], ); $products->appends(array_filter( ['q' => $query, 'sort' => $sort?->value], fn ($value) => $value !== null, )); // Sort dropdown links: same query, swapped sort (default sort => no param). $sortOptions = ProductSortOptions::build( $sort, fn (?ProductSort $option) => route('search', array_filter( ['q' => $query, 'sort' => $option?->value], fn ($value) => $value !== null, )), ); return view('search.index', [ 'query' => $query, 'products' => $products, 'sortOptions' => $sortOptions, ]); } /** * Full-text matches as product-card arrays. ProductSearchService returns * hydrated models in relevance order with no sort or pagination, so ordering * is done here in PHP and the controller paginates the mapped collection. * * @return Collection> */ private function cardsFor(string $query, ?ProductSort $sort): Collection { $results = $this->search->search($query)->load(['variants.prices', 'media']); return $this->sortResults($results, $sort) ->map(ProductCard::fromModel(...)) ->values(); } /** * @param EloquentCollection $results * @return EloquentCollection */ private function sortResults(EloquentCollection $results, ?ProductSort $sort): EloquentCollection { $price = fn (Product $product) => $product->variants->first()?->prices->first()?->price->value ?? 0; return match ($sort) { ProductSort::PriceAsc => $results->sortBy($price)->values(), ProductSort::PriceDesc => $results->sortByDesc($price)->values(), ProductSort::Newest => $results->sortByDesc('created_at')->values(), default => $results, // Meilisearch relevance order }; } }