117 lines
5.6 KiB
Markdown
117 lines
5.6 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. |
|
|
| `ancestors` | `$model->ancestors` | Display only. Array of `{id, name}`, ordered root-first — a breadcrumb (`Home > Apparel > Keychains`) can render directly from a single `getById()`/`getBySlug()` call, no extra queries. Empty array for a top-level collection. |
|
|
| `product_count` | Queried from the *product* Meilisearch index at collection-index time | Display only. How many products are in this collection **or any of its descendants** — matches what `ProductService::list(ProductFilters(collectionId: ...))` would return, not just direct assignment. Computed via `Product::search('')->options(['filter' => "collection_ids = \"{id}\""])`, so it depends on the product index already being current — reindex products *before* collections (see "Gotchas" below). |
|
|
|
|
`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".
|
|
|
|
**`product_count` needs the product index reindexed first.** `config/lunar/search.php`'s
|
|
`indexers` array is typically ordered `Collection` before `Product`, so a plain
|
|
`lunar:search:index --refresh` computes `product_count` against whatever the product
|
|
index held *before* this run — stale if products changed too. `lunar:search:index`
|
|
takes an explicit model list as its argument (`--ignore` restricts it to only those),
|
|
so reindex products first, then collections, when both need a fresh `--refresh` in the
|
|
same deploy:
|
|
|
|
```
|
|
php artisan lunar:search:index "Lunar\Models\Product" --ignore --refresh
|
|
php artisan lunar:search:index "Lunar\Models\Collection" --ignore --refresh
|
|
```
|
|
|
|
---
|
|
|
|
## 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.
|