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:
@@ -2,26 +2,62 @@
|
||||
|
||||
namespace Modules\Core\Search;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Lunar\Models\Currency;
|
||||
use Lunar\Models\Product;
|
||||
use Lunar\Search\ProductIndexer as BaseProductIndexer;
|
||||
|
||||
/**
|
||||
* Lunar's own indexer puts raw attribute HTML (e.g. name_en, description_en) into
|
||||
* the search index, which pollutes relevance ranking and highlighting with markup.
|
||||
* Strip tags from string fields before they reach Meilisearch.
|
||||
* Extends Lunar's own indexer to add fields needed for storefront listing/filtering
|
||||
* (Modules\Core\Catalog\ProductService) that aren't part of Lunar's default document:
|
||||
* collection membership and a comparable price. Neither is filterable in Meilisearch
|
||||
* until `php artisan lunar:meilisearch:setup` re-syncs index settings and the index is
|
||||
* refreshed — see docs/product-listing.md.
|
||||
*/
|
||||
class ProductIndexer extends BaseProductIndexer
|
||||
{
|
||||
public function getFilterableFields(): array
|
||||
{
|
||||
return [
|
||||
...parent::getFilterableFields(),
|
||||
'brand',
|
||||
'collections',
|
||||
'price',
|
||||
];
|
||||
}
|
||||
|
||||
public function makeAllSearchableUsing(Builder $query): Builder
|
||||
{
|
||||
return parent::makeAllSearchableUsing($query)->with(['collections', 'variants.prices']);
|
||||
}
|
||||
|
||||
public function toSearchableArray(Model $model): array
|
||||
{
|
||||
/** @var Product $model */
|
||||
$data = parent::toSearchableArray($model);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$data[$key] = trim(strip_tags($value));
|
||||
}
|
||||
}
|
||||
$data['collections'] = $model->collections->pluck('id')->map(fn ($id) => (string) $id)->all();
|
||||
$data['price'] = $this->cheapestPrice($model);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cheapest variant's base price (no customer group) in the default currency,
|
||||
* as a float in major units — e.g. 19.99, not 1999. Null if the product has no
|
||||
* variant with a price in that currency yet, so it's excluded from price filters
|
||||
* rather than sorting to the bottom as if it were free.
|
||||
*/
|
||||
private function cheapestPrice(Product $model): ?float
|
||||
{
|
||||
$currency = Currency::getDefault();
|
||||
|
||||
$price = $model->variants
|
||||
->flatMap(fn ($variant) => $variant->prices)
|
||||
->filter(fn ($price) => $price->currency_id === $currency->id && $price->customer_group_id === null)
|
||||
->min(fn ($price) => $price->price->value);
|
||||
|
||||
return $price !== null ? $price / (10 ** $currency->decimal_places) : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user