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,
|
'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
|
## 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
|
```php
|
||||||
// config/core.php
|
use Modules\Core\Product\Services\ProductOptionTypeManager;
|
||||||
'product_option_types' => [
|
|
||||||
|
ProductOptionTypeManager::get()->register([
|
||||||
\App\ProductOptions\ColorOptionType::class,
|
\App\ProductOptions\ColorOptionType::class,
|
||||||
],
|
]);
|
||||||
```
|
```
|
||||||
|
|
||||||
This is a plain list, **not** keyed by `ProductOption::handle` — a shop's own handle
|
Not a published config array — the mapping isn't per-`ProductOption`, so there's
|
||||||
naming (transliterated Greek, legacy import slugs, whatever an admin happened to type
|
nothing for a shop to *key* by. Instead, an admin picks a type per-option from a
|
||||||
when creating the option) shouldn't have to match a type's key. Instead, an admin
|
dropdown on the `ProductOption` edit form itself (see below); the choice is stored
|
||||||
picks a type per-option from a dropdown on the `ProductOption` edit form itself (see
|
in `ProductOption::meta['option_type']`, deliberately **not** tied to the option's
|
||||||
below); the choice is stored in `ProductOption::meta['option_type']`, not inferred
|
`handle` (a shop's own handle naming — transliterated Greek, legacy import slugs —
|
||||||
from anything else.
|
shouldn't have to match a type's key).
|
||||||
|
|
||||||
A `ProductOption` with no type selected behaves exactly as stock Lunar does — plain
|
A `ProductOption` with no type selected behaves exactly as stock Lunar does — plain
|
||||||
name/position, no extra meta form.
|
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
|
dropdown and in `ProductOption::meta['option_type']` — it has no relationship to the
|
||||||
`ProductOption::handle`.
|
`ProductOption::handle`.
|
||||||
|
|
||||||
A reference implementation ships at `Modules\Core\Product\OptionTypes\ColorOptionType`
|
A reference implementation ships at `Modules\Core\Product\OptionTypes\ColorOptionType`,
|
||||||
— not auto-registered, since registration is always an explicit shop decision.
|
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
|
## How it's wired into the admin UI
|
||||||
|
|
||||||
`Modules\Core\Product\Services\ProductOptionTypeManager`:
|
`Modules\Core\Product\Services\ProductOptionTypeManager` is a singleton registry:
|
||||||
- `all(): Collection<string, ProductOptionTypeInterface>` — every enabled type,
|
- `get(): static` — the shared instance.
|
||||||
keyed by `getKey()`.
|
- `register(array $types): void` — registers one or more type classes, keyed
|
||||||
- `resolve(?string $key): ?ProductOptionTypeInterface` — looks up one by key (or
|
internally by `getKey()`.
|
||||||
`null` if no key / not found).
|
- `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
|
Two extensions hook into Lunar's admin via its extension system
|
||||||
(`LunarPanel::extensions([...])`, registered in `CorePlugin`) — no forking of Lunar's
|
(`LunarPanel::extensions([...])`, registered in `CorePlugin`) — no forking of Lunar's
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ProductOptionResourceExtension extends ResourceExtension
|
|||||||
{
|
{
|
||||||
public function extendForm(Form $form): Form
|
public function extendForm(Form $form): Form
|
||||||
{
|
{
|
||||||
$options = app(ProductOptionTypeManager::class)->all()
|
$options = collect(ProductOptionTypeManager::get()->all())
|
||||||
->keys()
|
->keys()
|
||||||
->mapWithKeys(fn (string $key) => [$key => Str::headline($key)])
|
->mapWithKeys(fn (string $key) => [$key => Str::headline($key)])
|
||||||
->all();
|
->all();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ValuesRelationManagerExtension extends RelationManagerExtension
|
|||||||
/** @var ProductOption $option */
|
/** @var ProductOption $option */
|
||||||
$option = $this->caller->getOwnerRecord();
|
$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) {
|
if ($type === null) {
|
||||||
return $form;
|
return $form;
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ use Filament\Forms\Components\ColorPicker;
|
|||||||
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
|
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference implementation: describes a 'color' ProductOption's values as
|
* Describes a 'color' ProductOption's values as carrying a hex code in
|
||||||
* carrying a hex code in `meta.hex`, editable via a Filament color picker.
|
* `meta.hex`, editable via a Filament color picker. Registered automatically by
|
||||||
* Not auto-registered — a shop opts in via config('core.product_option_types').
|
* `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
|
class ColorOptionType implements ProductOptionTypeInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class ProductIndexer extends BaseProductIndexer
|
|||||||
'purchasable' => $variant->purchasable,
|
'purchasable' => $variant->purchasable,
|
||||||
'options' => $variant->values->map(fn ($value) => [
|
'options' => $variant->values->map(fn ($value) => [
|
||||||
'option' => $this->translatedName($value->option->name),
|
'option' => $this->translatedName($value->option->name),
|
||||||
|
'handle' => $value->option->handle,
|
||||||
'value' => $this->translatedName($value->name),
|
'value' => $this->translatedName($value->name),
|
||||||
'meta' => $value->meta,
|
'meta' => $value->meta,
|
||||||
])->all(),
|
])->all(),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Product\Services;
|
namespace Modules\Core\Product\Services;
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
|
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
|
* 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.
|
* 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,
|
* A singleton registry, same shape as `Modules\Core\Notification\NotificationRegistry`
|
||||||
* not a config array, because the mapping from option to type is an admin's per-option
|
* — a consuming app calls `ProductOptionTypeManager::get()->register([...])` from its
|
||||||
* choice made in the UI (see ValuesRelationManagerExtension/ProductOptionResourceExtension),
|
* own service provider `boot()`, rather than listing classes in a published config
|
||||||
* not something config alone can express.
|
* file.
|
||||||
*/
|
*/
|
||||||
class ProductOptionTypeManager
|
class ProductOptionTypeManager
|
||||||
{
|
{
|
||||||
/**
|
private static ?self $instance = null;
|
||||||
* @return Collection<string, ProductOptionTypeInterface> keyed by getKey()
|
|
||||||
*/
|
/** @var array<string, class-string<ProductOptionTypeInterface>> */
|
||||||
public function all(): Collection
|
private array $types = [];
|
||||||
|
|
||||||
|
private function __construct() {}
|
||||||
|
|
||||||
|
public static function get(): static
|
||||||
{
|
{
|
||||||
return collect(config('core.product_option_types', []))
|
if (static::$instance === null) {
|
||||||
->map(fn (string $class) => app($class))
|
static::$instance = new static();
|
||||||
->keyBy(fn (ProductOptionTypeInterface $type) => $type::getKey());
|
}
|
||||||
|
|
||||||
|
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
|
public function resolve(?string $key): ?ProductOptionTypeInterface
|
||||||
{
|
{
|
||||||
if ($key === null) {
|
if ($key === null || ! isset($this->types[$key])) {
|
||||||
return null;
|
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\ProductOption;
|
||||||
use Lunar\Models\ProductOptionValue;
|
use Lunar\Models\ProductOptionValue;
|
||||||
use Modules\Core\Product\Observers\ProductOptionReindexObserver;
|
use Modules\Core\Product\Observers\ProductOptionReindexObserver;
|
||||||
|
use Modules\Core\Product\OptionTypes\ColorOptionType;
|
||||||
|
use Modules\Core\Product\Services\ProductOptionTypeManager;
|
||||||
|
|
||||||
class ProductServiceProvider extends ServiceProvider
|
class ProductServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
ProductOptionTypeManager::get()->register([
|
||||||
|
ColorOptionType::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$observer = new ProductOptionReindexObserver;
|
$observer = new ProductOptionReindexObserver;
|
||||||
|
|
||||||
ProductOption::saved(fn (ProductOption $option) => $observer->optionSaved($option));
|
ProductOption::saved(fn (ProductOption $option) => $observer->optionSaved($option));
|
||||||
|
|||||||
Reference in New Issue
Block a user