Fix: Updating Product Options Registry and adding handle to the meilisearch index
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,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));
|
||||
|
||||
Reference in New Issue
Block a user