From e95ea4a43cac2bf4bde3233c9b13f4d1f9488bbc Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Thu, 27 Aug 2026 11:14:56 +0300 Subject: [PATCH] Fix: Updating Product Options Registry and adding handle to the meilisearch index --- config/core.php | 19 ------- docs/product-options.md | 42 ++++++++------ .../ProductOptionResourceExtension.php | 2 +- .../ValuesRelationManagerExtension.php | 2 +- src/Product/OptionTypes/ColorOptionType.php | 7 ++- src/Product/Services/ProductIndexer.php | 1 + .../Services/ProductOptionTypeManager.php | 56 ++++++++++++++----- src/Providers/ProductServiceProvider.php | 6 ++ 8 files changed, 80 insertions(+), 55 deletions(-) diff --git a/config/core.php b/config/core.php index bcb16f1..5e0f027 100644 --- a/config/core.php +++ b/config/core.php @@ -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' => [], - ]; diff --git a/docs/product-options.md b/docs/product-options.md index f1043fb..9b9a436 100644 --- a/docs/product-options.md +++ b/docs/product-options.md @@ -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` — 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` — 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 diff --git a/src/Product/Filament/Extensions/ProductOptionResourceExtension.php b/src/Product/Filament/Extensions/ProductOptionResourceExtension.php index 0c9635f..8b2f500 100644 --- a/src/Product/Filament/Extensions/ProductOptionResourceExtension.php +++ b/src/Product/Filament/Extensions/ProductOptionResourceExtension.php @@ -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(); diff --git a/src/Product/Filament/Extensions/ValuesRelationManagerExtension.php b/src/Product/Filament/Extensions/ValuesRelationManagerExtension.php index 17833a4..6fdcaa5 100644 --- a/src/Product/Filament/Extensions/ValuesRelationManagerExtension.php +++ b/src/Product/Filament/Extensions/ValuesRelationManagerExtension.php @@ -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; diff --git a/src/Product/OptionTypes/ColorOptionType.php b/src/Product/OptionTypes/ColorOptionType.php index e4c6fec..06b70d5 100644 --- a/src/Product/OptionTypes/ColorOptionType.php +++ b/src/Product/OptionTypes/ColorOptionType.php @@ -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 { diff --git a/src/Product/Services/ProductIndexer.php b/src/Product/Services/ProductIndexer.php index 7928151..68a15f7 100644 --- a/src/Product/Services/ProductIndexer.php +++ b/src/Product/Services/ProductIndexer.php @@ -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(), diff --git a/src/Product/Services/ProductOptionTypeManager.php b/src/Product/Services/ProductOptionTypeManager.php index 258a900..1d73ee7 100644 --- a/src/Product/Services/ProductOptionTypeManager.php +++ b/src/Product/Services/ProductOptionTypeManager.php @@ -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 keyed by getKey() - */ - public function all(): Collection + private static ?self $instance = null; + + /** @var array> */ + 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> $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> + */ + public function all(): array + { + return $this->types; } } diff --git a/src/Providers/ProductServiceProvider.php b/src/Providers/ProductServiceProvider.php index b45cd56..712d9ec 100644 --- a/src/Providers/ProductServiceProvider.php +++ b/src/Providers/ProductServiceProvider.php @@ -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));