Feature: Adding Product Service to Lunar
This commit introduces a Product service to Lunar. This product service calls Meilisearch to fetch an indexed product. The indexer has been updated to also include the collection and the price of the product. A product search service has also been created to be used by the frontend's search
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Catalog;
|
||||
|
||||
/**
|
||||
* Filter input for ProductService::list(). All fields are optional — omitted
|
||||
* filters are simply not added to the Meilisearch query. Values are matched
|
||||
* against Modules\Core\Search\ProductIndexer's document fields, so filtering
|
||||
* only works on stores where that indexer is registered and the index has
|
||||
* been re-synced (see docs/product-listing.md).
|
||||
*/
|
||||
class ProductFilters
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?int $collectionId = null,
|
||||
public readonly ?string $brand = null,
|
||||
public readonly ?float $minPrice = null,
|
||||
public readonly ?float $maxPrice = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Catalog;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Lunar\Models\Product;
|
||||
|
||||
/**
|
||||
* Storefront product listing/filtering, reading directly from the Meilisearch index
|
||||
* (Modules\Core\Search\ProductIndexer) rather than the database — no ->get() model
|
||||
* hydration, so callers get plain arrays of the indexed document, not Eloquent models.
|
||||
* Consumers needing the full record (e.g. a product detail page) should look the
|
||||
* product up directly via Lunar's Product model instead.
|
||||
*
|
||||
* Full-text query search lives separately in Modules\Core\Search\ProductSearchService;
|
||||
* this service is for browsing/filtering without a search term.
|
||||
*/
|
||||
class ProductService
|
||||
{
|
||||
/**
|
||||
* @return array{data: array<int, array>, meta: array}
|
||||
*/
|
||||
public function list(?ProductFilters $filters = null, int $perPage = 24, int $page = 1): array
|
||||
{
|
||||
$paginator = Product::search('')
|
||||
->options([
|
||||
'filter' => $this->buildFilter($filters),
|
||||
])
|
||||
->paginateRaw(perPage: $perPage, page: $page);
|
||||
|
||||
// For the Meilisearch driver, Scout's paginateRaw() puts the whole raw
|
||||
// response (hits, query, processingTimeMs, ...) in items(), not a plain
|
||||
// list of hits — the actual documents are under the 'hits' key.
|
||||
$rawResponse = $paginator->items();
|
||||
|
||||
return [
|
||||
'data' => collect($rawResponse['hits'] ?? [])->values()->all(),
|
||||
'meta' => [
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function buildFilter(?ProductFilters $filters): ?string
|
||||
{
|
||||
if ($filters === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$clauses = Collection::make([
|
||||
$filters->collectionId !== null ? "collections = \"{$filters->collectionId}\"" : null,
|
||||
$filters->brand !== null ? 'brand = "'.addcslashes($filters->brand, '"\\').'"' : null,
|
||||
$filters->minPrice !== null ? "price >= {$filters->minPrice}" : null,
|
||||
$filters->maxPrice !== null ? "price <= {$filters->maxPrice}" : null,
|
||||
])->filter();
|
||||
|
||||
return $clauses->isEmpty() ? null : $clauses->join(' AND ');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user