2026-07-12 05:34:43 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Search;
|
|
|
|
|
|
2026-08-24 12:41:28 +03:00
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2026-07-12 05:34:43 +03:00
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2026-08-24 12:41:28 +03:00
|
|
|
use Lunar\Models\Currency;
|
|
|
|
|
use Lunar\Models\Product;
|
2026-07-12 05:34:43 +03:00
|
|
|
use Lunar\Search\ProductIndexer as BaseProductIndexer;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-24 12:41:28 +03:00
|
|
|
* 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.
|
2026-07-12 05:34:43 +03:00
|
|
|
*/
|
|
|
|
|
class ProductIndexer extends BaseProductIndexer
|
|
|
|
|
{
|
2026-08-24 12:41:28 +03:00
|
|
|
public function getFilterableFields(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
...parent::getFilterableFields(),
|
|
|
|
|
'brand',
|
|
|
|
|
'collections',
|
|
|
|
|
'price',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function makeAllSearchableUsing(Builder $query): Builder
|
|
|
|
|
{
|
|
|
|
|
return parent::makeAllSearchableUsing($query)->with(['collections', 'variants.prices']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 05:34:43 +03:00
|
|
|
public function toSearchableArray(Model $model): array
|
|
|
|
|
{
|
2026-08-24 12:41:28 +03:00
|
|
|
/** @var Product $model */
|
2026-07-12 05:34:43 +03:00
|
|
|
$data = parent::toSearchableArray($model);
|
|
|
|
|
|
2026-08-24 12:41:28 +03:00
|
|
|
$data['collections'] = $model->collections->pluck('id')->map(fn ($id) => (string) $id)->all();
|
|
|
|
|
$data['price'] = $this->cheapestPrice($model);
|
2026-07-12 05:34:43 +03:00
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
2026-08-24 12:41:28 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
2026-07-12 05:34:43 +03:00
|
|
|
}
|