102 lines
4.2 KiB
Markdown
102 lines
4.2 KiB
Markdown
# Collections
|
|||
|
|
|
||
|
|
`Modules\Core\Catalog\Services\CollectionService` provides category browsing/nav AND
|
||
|
|
single-collection lookup for a storefront — `list()`, `getById()`, `getBySlug()` —
|
||
|
|
all reading directly from the Meilisearch index, mirroring
|
||
|
|
`Modules\Core\Catalog\Services\ProductService` (see `product-listing.md`) exactly.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Why it reads from the index, not the database
|
||
|
|
|
||
|
|
Lunar's own `Lunar\Search\CollectionIndexer` only carries `id`/`name`/`created_at` —
|
||
|
|
nowhere near enough for a storefront category page or a nav tree.
|
||
|
|
`Modules\Core\Catalog\Services\CollectionIndexer` extends it to add everything
|
||
|
|
`CollectionService` needs:
|
||
|
|
|
||
|
|
| Field | Source | Notes |
|
||
|
|
|---|---|---|
|
||
|
|
| `parent_id` | `$model->parent_id` | Filterable. The nested-set tree's parent pointer — `null` for a top-level collection. |
|
||
|
|
| `_lft` | `$model->_lft` | Filterable and sortable. The nested-set tree position — lets `CollectionService` resolve tree order without a database read. |
|
||
|
|
| `collection_group_id` | `$model->collection_group_id` | Filterable. Mirrors `Collection::scopeInGroup()`. |
|
||
|
|
| `slugs` | `$model->urls->pluck('slug')` | Filterable. Every locale's `Url::slug`, so `getBySlug()` resolves purely from the index. |
|
||
|
|
| `thumbnail` | `$model->getThumbnailImage()` | Display only. `null` if the collection has no thumbnail image. |
|
||
|
|
|
||
|
|
`name`/`description` (and any other `TranslatedText` attribute) are indexed per-locale
|
||
|
|
by Lunar's base indexer and resolved by `CollectionService` exactly like
|
||
|
|
`ProductService` does — see `product-listing.md`'s "Locale resolution" section, same
|
||
|
|
logic, same `LanguageCache::defaultLocale()` fallback.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Modules\Core\Catalog\DTOs\CollectionFilters;
|
||
|
|
use Modules\Core\Catalog\Enums\CollectionSort;
|
||
|
|
use Modules\Core\Catalog\Services\CollectionService;
|
||
|
|
|
||
|
|
$service = app(CollectionService::class);
|
||
|
|
|
||
|
|
// Top-level collections only (parent_id IS NULL) — for building a nav tree
|
||
|
|
$roots = $service->list(
|
||
|
|
filters: new CollectionFilters(rootOnly: true),
|
||
|
|
sort: CollectionSort::Position,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Children of a specific collection
|
||
|
|
$children = $service->list(
|
||
|
|
filters: new CollectionFilters(parentId: 222),
|
||
|
|
sort: CollectionSort::Position,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Filter by collection group
|
||
|
|
$collections = $service->list(filters: new CollectionFilters(groupId: 4));
|
||
|
|
|
||
|
|
// Single collection, by primary key or slug
|
||
|
|
$collection = $service->getById(223);
|
||
|
|
$collection = $service->getBySlug('keychains');
|
||
|
|
```
|
||
|
|
|
||
|
|
`CollectionFilters(parentId: ..., rootOnly: ...)` are mutually exclusive — if both are
|
||
|
|
set, `parentId` wins. There's no `parentId: null` shorthand for "root only", since
|
||
|
|
that would be ambiguous with "don't filter by parent at all" (the DTO's actual
|
||
|
|
default); `rootOnly` names the root-collections case explicitly instead.
|
||
|
|
|
||
|
|
`CollectionSort::Position` (`_lft:asc`) is the recommended default for any nav/tree
|
||
|
|
UI — it matches the order an admin arranges collections in Lunar's own Filament UI.
|
||
|
|
`Name` and `Newest` are also available, mirroring `ProductSort`'s shape.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Registration
|
||
|
|
|
||
|
|
Like `ProductIndexer`, `CollectionIndexer` must be registered in the consuming app's
|
||
|
|
own `config/lunar/search.php`:
|
||
|
|
|
||
|
|
```php
|
||
|
|
'indexers' => [
|
||
|
|
Lunar\Models\Collection::class => Modules\Core\Catalog\Services\CollectionIndexer::class,
|
||
|
|
// ...
|
||
|
|
],
|
||
|
|
```
|
||
|
|
|
||
|
|
New/changed fields aren't filterable/sortable in Meilisearch until `php artisan
|
||
|
|
lunar:meilisearch:setup` re-syncs index settings, and existing documents need
|
||
|
|
`lunar:search:index --refresh` to pick up the new shape. If `SCOUT_QUEUE` is enabled,
|
||
|
|
the queue worker also needs restarting after deploying changes to the indexer class —
|
||
|
|
see `docs/lunar.md` "Gotchas".
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## When to still use Eloquent directly
|
||
|
|
|
||
|
|
A single collection's full detail page (breadcrumb via `$collection->breadcrumb`,
|
||
|
|
tree ancestors/descendants, route-model-bound `Collection $collection` in a
|
||
|
|
controller signature) should keep reading Eloquent directly rather than going through
|
||
|
|
`CollectionService` — the indexed document doesn't carry ancestor chains or the full
|
||
|
|
nested-set relations, and route-model binding already gives a controller the full
|
||
|
|
model for free. `CollectionService` is for browsing/listing and lightweight
|
||
|
|
by-id/by-slug lookups where a full Eloquent hydration would be wasteful, the same
|
||
|
|
tradeoff `ProductService` makes for products.
|