Fix: Updating Product Options Registry and adding handle to the meilisearch index

This commit is contained in:
2026-08-27 11:14:56 +03:00
parent cb3fe095d9
commit e95ea4a43c
8 changed files with 80 additions and 55 deletions
-19
View File
@@ -16,23 +16,4 @@ return [
'auto_create_customer_for_user' => true,
/*
|--------------------------------------------------------------------------
| Product Option Types
|--------------------------------------------------------------------------
|
| Enabled `Modules\Core\Product\Contracts\ProductOptionTypeInterface`
| implementations, describing what structured data a ProductOption's
| values carry in their `meta` jsonb column, and how an admin edits it.
| An admin picks one per ProductOption from a dropdown built from this
| list (stored in ProductOption::meta, not tied to the option's handle) —
| a ProductOption with none selected has no described meta behavior,
| plain name/position only.
|
| \App\ProductOptions\ColorOptionType::class,
|
*/
'product_option_types' => [],
];
+25 -17
View File
@@ -15,21 +15,23 @@ admin edits that data — without introducing a new model. `ProductOption`/
## Registering a type
A shop enables a type class in `config/core.php`:
A shop registers a type class from its own service provider's `boot()`, the same
shape as `Modules\Core\Notification\NotificationRegistry`:
```php
// config/core.php
'product_option_types' => [
use Modules\Core\Product\Services\ProductOptionTypeManager;
ProductOptionTypeManager::get()->register([
\App\ProductOptions\ColorOptionType::class,
],
]);
```
This is a plain list, **not** keyed by `ProductOption::handle` — a shop's own handle
naming (transliterated Greek, legacy import slugs, whatever an admin happened to type
when creating the option) shouldn't have to match a type's key. Instead, an admin
picks a type per-option from a dropdown on the `ProductOption` edit form itself (see
below); the choice is stored in `ProductOption::meta['option_type']`, not inferred
from anything else.
Not a published config array — the mapping isn't per-`ProductOption`, so there's
nothing for a shop to *key* by. Instead, an admin picks a type per-option from a
dropdown on the `ProductOption` edit form itself (see below); the choice is stored
in `ProductOption::meta['option_type']`, deliberately **not** tied to the option's
`handle` (a shop's own handle naming — transliterated Greek, legacy import slugs —
shouldn't have to match a type's key).
A `ProductOption` with no type selected behaves exactly as stock Lunar does — plain
name/position, no extra meta form.
@@ -68,18 +70,24 @@ plain jsonb column). `getKey()` is the identifier used in the admin's "Option Ty
dropdown and in `ProductOption::meta['option_type']` — it has no relationship to the
`ProductOption::handle`.
A reference implementation ships at `Modules\Core\Product\OptionTypes\ColorOptionType`
— not auto-registered, since registration is always an explicit shop decision.
A reference implementation ships at `Modules\Core\Product\OptionTypes\ColorOptionType`,
registered automatically by `Modules\Core\Providers\ProductServiceProvider` — no shop
setup needed for it to appear in the "Option Type" dropdown, though an admin still
has to pick it per-`ProductOption` for it to take effect.
---
## How it's wired into the admin UI
`Modules\Core\Product\Services\ProductOptionTypeManager`:
- `all(): Collection<string, ProductOptionTypeInterface>` — every enabled type,
keyed by `getKey()`.
- `resolve(?string $key): ?ProductOptionTypeInterface` — looks up one by key (or
`null` if no key / not found).
`Modules\Core\Product\Services\ProductOptionTypeManager` is a singleton registry:
- `get(): static` — the shared instance.
- `register(array $types): void` — registers one or more type classes, keyed
internally by `getKey()`.
- `unregister(string $key): void`
- `resolve(?string $key): ?ProductOptionTypeInterface` — looks up a registered type
by key (or `null` if no key / not found).
- `all(): array<string, class-string>` — every registered type's class, keyed by
`getKey()`.
Two extensions hook into Lunar's admin via its extension system
(`LunarPanel::extensions([...])`, registered in `CorePlugin`) — no forking of Lunar's
@@ -18,7 +18,7 @@ class ProductOptionResourceExtension extends ResourceExtension
{
public function extendForm(Form $form): Form
{
$options = app(ProductOptionTypeManager::class)->all()
$options = collect(ProductOptionTypeManager::get()->all())
->keys()
->mapWithKeys(fn (string $key) => [$key => Str::headline($key)])
->all();
@@ -20,7 +20,7 @@ class ValuesRelationManagerExtension extends RelationManagerExtension
/** @var ProductOption $option */
$option = $this->caller->getOwnerRecord();
$type = app(ProductOptionTypeManager::class)->resolve($option->meta['option_type'] ?? null);
$type = ProductOptionTypeManager::get()->resolve($option->meta['option_type'] ?? null);
if ($type === null) {
return $form;
+4 -3
View File
@@ -6,9 +6,10 @@ use Filament\Forms\Components\ColorPicker;
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
/**
* Reference implementation: describes a 'color' ProductOption's values as
* carrying a hex code in `meta.hex`, editable via a Filament color picker.
* Not auto-registered — a shop opts in via config('core.product_option_types').
* Describes a 'color' ProductOption's values as carrying a hex code in
* `meta.hex`, editable via a Filament color picker. Registered automatically by
* `Modules\Core\Providers\ProductServiceProvider` — a shop's admin still has to
* pick "Color" from the Option Type dropdown per-ProductOption for it to apply.
*/
class ColorOptionType implements ProductOptionTypeInterface
{
+1
View File
@@ -112,6 +112,7 @@ class ProductIndexer extends BaseProductIndexer
'purchasable' => $variant->purchasable,
'options' => $variant->values->map(fn ($value) => [
'option' => $this->translatedName($value->option->name),
'handle' => $value->option->handle,
'value' => $this->translatedName($value->name),
'meta' => $value->meta,
])->all(),
@@ -2,7 +2,6 @@
namespace Modules\Core\Product\Services;
use Illuminate\Support\Collection;
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
/**
@@ -12,29 +11,58 @@ use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
* tied to the option's `handle`, since a shop's own handle naming (e.g. transliterated
* Greek, legacy imports) shouldn't have to match a type's key.
*
* The available keys come from `config('core.product_option_types')` — a plain list,
* not a config array, because the mapping from option to type is an admin's per-option
* choice made in the UI (see ValuesRelationManagerExtension/ProductOptionResourceExtension),
* not something config alone can express.
* A singleton registry, same shape as `Modules\Core\Notification\NotificationRegistry`
* — a consuming app calls `ProductOptionTypeManager::get()->register([...])` from its
* own service provider `boot()`, rather than listing classes in a published config
* file.
*/
class ProductOptionTypeManager
{
/**
* @return Collection<string, ProductOptionTypeInterface> keyed by getKey()
*/
public function all(): Collection
private static ?self $instance = null;
/** @var array<string, class-string<ProductOptionTypeInterface>> */
private array $types = [];
private function __construct() {}
public static function get(): static
{
return collect(config('core.product_option_types', []))
->map(fn (string $class) => app($class))
->keyBy(fn (ProductOptionTypeInterface $type) => $type::getKey());
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
/**
* @param array<class-string<ProductOptionTypeInterface>> $types
*/
public function register(array $types): void
{
foreach ($types as $class) {
$this->types[$class::getKey()] = $class;
}
}
public function unregister(string $key): void
{
unset($this->types[$key]);
}
public function resolve(?string $key): ?ProductOptionTypeInterface
{
if ($key === null) {
if ($key === null || ! isset($this->types[$key])) {
return null;
}
return $this->all()->get($key);
return app($this->types[$key]);
}
/**
* @return array<string, class-string<ProductOptionTypeInterface>>
*/
public function all(): array
{
return $this->types;
}
}
+6
View File
@@ -6,11 +6,17 @@ use Illuminate\Support\ServiceProvider;
use Lunar\Models\ProductOption;
use Lunar\Models\ProductOptionValue;
use Modules\Core\Product\Observers\ProductOptionReindexObserver;
use Modules\Core\Product\OptionTypes\ColorOptionType;
use Modules\Core\Product\Services\ProductOptionTypeManager;
class ProductServiceProvider extends ServiceProvider
{
public function boot(): void
{
ProductOptionTypeManager::get()->register([
ColorOptionType::class,
]);
$observer = new ProductOptionReindexObserver;
ProductOption::saved(fn (ProductOption $option) => $observer->optionSaved($option));