$this->buildFilter($filters)]; if ($sort !== null) { $options['sort'] = [$sort->toMeilisearchSort()]; } $paginator = CollectionModel::search('') ->options($options) ->paginateRaw(perPage: $perPage, page: $page); $data = collect($this->hitsFrom($paginator)) ->map(fn (array $collection) => $this->withLocalizedFields($collection)) ->all(); return new LengthAwarePaginator( items: $data, total: $paginator->total(), perPage: $paginator->perPage(), currentPage: $paginator->currentPage(), options: ['path' => LengthAwarePaginator::resolveCurrentPath()], ); } /** * Look up a single collection by its URL slug (any locale). Returns the full * indexed collection document, or null if no collection has that slug. */ public function getBySlug(string $slug): ?array { return $this->findOneWhere('slugs = "'.addcslashes($slug, '"\\').'"'); } /** * Look up a single collection by its primary key. Returns the full indexed * collection document, or null if no collection has that id. */ public function getById(int $id): ?array { return $this->findOneWhere("id = \"{$id}\""); } private function findOneWhere(string $filter): ?array { $paginator = CollectionModel::search('') ->options(['filter' => $filter]) ->paginateRaw(perPage: 1, page: 1); $collection = $this->hitsFrom($paginator)[0] ?? null; return $collection !== null ? $this->withLocalizedFields($collection) : null; } /** * Resolves every translated Collection attribute's current-locale value — same * logic as ProductService::withLocalizedFields(), see there for the full * reasoning (AttributeManifest-driven, store-default-locale fallback, raw * per-locale keys stripped after resolving). */ private function withLocalizedFields(array $collection): array { $locale = App::getLocale(); $fallbackLocale = $this->languages->defaultLocale(); $availableLocales = $this->languages->availableLocales(); foreach ($this->translatedAttributeHandles() as $handle) { $collection[$handle] = $collection[$handle.'_'.$locale] ?? $collection[$handle.'_'.$fallbackLocale] ?? null; foreach ($availableLocales as $availableLocale) { unset($collection[$handle.'_'.$availableLocale]); } } return $collection; } /** * @return array */ private function translatedAttributeHandles(): array { return $this->attributes->getSearchableAttributes((new CollectionModel)->getMorphClass()) ->filter(fn ($attribute) => $attribute->type === TranslatedText::class) ->pluck('handle') ->all(); } /** * For the Meilisearch driver, Scout's paginateRaw() puts the whole raw response * in items(), not a plain list of hits — see ProductService's identical note. */ private function hitsFrom(LengthAwarePaginatorContract $paginator): array { $rawResponse = $paginator->items(); return collect($rawResponse['hits'] ?? [])->values()->all(); } private function buildFilter(?CollectionFilters $filters): ?string { if ($filters === null) { return null; } $clauses = Collection::make([ $filters->parentId !== null ? "parent_id = \"{$filters->parentId}\"" : ($filters->rootOnly ? 'parent_id IS NULL' : null), $filters->groupId !== null ? "collection_group_id = \"{$filters->groupId}\"" : null, ])->filter(); return $clauses->isEmpty() ? null : $clauses->join(' AND '); } }