148 lines
5.3 KiB
PHP
148 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Catalog\Services;
|
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorContract;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\App;
|
|
use Lunar\Base\AttributeManifest;
|
|
use Lunar\FieldTypes\TranslatedText;
|
|
use Lunar\Models\Collection as CollectionModel;
|
|
use Modules\Core\Catalog\DTOs\CollectionFilters;
|
|
use Modules\Core\Catalog\Enums\CollectionSort;
|
|
use Modules\Core\Localization\Services\LanguageCache;
|
|
|
|
/**
|
|
* Category browsing (tree/nav) AND single-collection lookup, all reading directly
|
|
* from the Meilisearch index (Modules\Core\Catalog\Services\CollectionIndexer) — same
|
|
* shape and reasoning as Modules\Core\Catalog\Services\ProductService. Callers get
|
|
* plain arrays of the indexed document, not Eloquent models.
|
|
*/
|
|
class CollectionService
|
|
{
|
|
public function __construct(
|
|
private readonly LanguageCache $languages,
|
|
private readonly AttributeManifest $attributes,
|
|
) {}
|
|
|
|
/**
|
|
* Returns a real LengthAwarePaginator (not Scout's own paginateRaw() result — see
|
|
* ProductService's "Meilisearch driver quirk" note) so a controller/view gets
|
|
* normal pagination behaviour without ever touching the raw Meilisearch response.
|
|
*/
|
|
public function list(?CollectionFilters $filters = null, int $perPage = 24, int $page = 1, ?CollectionSort $sort = null): LengthAwarePaginator
|
|
{
|
|
$options = ['filter' => $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<int, string>
|
|
*/
|
|
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 ');
|
|
}
|
|
}
|