2026-08-27 01:55:58 +03:00
|
|
|
# Product Option Types
|
|
|
|
|
|
|
|
|
|
Lunar's `ProductOption`/`ProductOptionValue` are generic by design — a "Color" option
|
|
|
|
|
and a "Size" option are both just a handle, a translated name, and a list of values.
|
|
|
|
|
Each `ProductOptionValue` carries a free-form `meta` jsonb column, but nothing in
|
|
|
|
|
Lunar's own admin UI exposes it — there's no way for an admin to, say, attach a hex
|
|
|
|
|
code to a "Red" value without editing the database directly.
|
|
|
|
|
|
|
|
|
|
`Modules\Core\Product\Contracts\ProductOptionTypeInterface` describes how a category
|
|
|
|
|
of option behaves — what structured data its values carry in `meta`, and how an
|
|
|
|
|
admin edits that data — without introducing a new model. `ProductOption`/
|
|
|
|
|
`ProductOptionValue` stay exactly as Lunar defines them.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Registering a type
|
|
|
|
|
|
2026-08-27 11:14:56 +03:00
|
|
|
A shop registers a type class from its own service provider's `boot()`, the same
|
|
|
|
|
shape as `Modules\Core\Notification\NotificationRegistry`:
|
2026-08-27 01:55:58 +03:00
|
|
|
|
|
|
|
|
```php
|
2026-08-27 11:14:56 +03:00
|
|
|
use Modules\Core\Product\Services\ProductOptionTypeManager;
|
|
|
|
|
|
|
|
|
|
ProductOptionTypeManager::get()->register([
|
2026-08-27 01:55:58 +03:00
|
|
|
\App\ProductOptions\ColorOptionType::class,
|
2026-08-27 11:14:56 +03:00
|
|
|
]);
|
2026-08-27 01:55:58 +03:00
|
|
|
```
|
|
|
|
|
|
2026-08-27 11:14:56 +03:00
|
|
|
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).
|
2026-08-27 01:55:58 +03:00
|
|
|
|
|
|
|
|
A `ProductOption` with no type selected behaves exactly as stock Lunar does — plain
|
|
|
|
|
name/position, no extra meta form.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Writing a type
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
namespace App\ProductOptions;
|
|
|
|
|
|
|
|
|
|
use Filament\Forms\Components\ColorPicker;
|
|
|
|
|
use Modules\Core\Product\Contracts\ProductOptionTypeInterface;
|
|
|
|
|
|
|
|
|
|
class ColorOptionType implements ProductOptionTypeInterface
|
|
|
|
|
{
|
|
|
|
|
public static function getKey(): string
|
|
|
|
|
{
|
|
|
|
|
return 'color';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getMetaForm(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
ColorPicker::make('meta.hex')
|
|
|
|
|
->label('Color')
|
|
|
|
|
->required(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`getMetaForm()` returns Filament form components, keyed under `meta.*` dot notation
|
|
|
|
|
— the path they save to on `ProductOptionValue::meta` (cast as `AsArrayObject`, a
|
|
|
|
|
plain jsonb column). `getKey()` is the identifier used in the admin's "Option Type"
|
|
|
|
|
dropdown and in `ProductOption::meta['option_type']` — it has no relationship to the
|
|
|
|
|
`ProductOption::handle`.
|
|
|
|
|
|
2026-08-27 11:14:56 +03:00
|
|
|
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.
|
2026-08-27 01:55:58 +03:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## How it's wired into the admin UI
|
|
|
|
|
|
2026-08-27 11:14:56 +03:00
|
|
|
`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()`.
|
2026-08-27 01:55:58 +03:00
|
|
|
|
|
|
|
|
Two extensions hook into Lunar's admin via its extension system
|
|
|
|
|
(`LunarPanel::extensions([...])`, registered in `CorePlugin`) — no forking of Lunar's
|
|
|
|
|
classes needed:
|
|
|
|
|
|
|
|
|
|
- `Modules\Core\Product\Filament\Extensions\ProductOptionResourceExtension` extends
|
|
|
|
|
`Lunar\Admin\Filament\Resources\ProductOptionResource`'s own form with a `Select`
|
|
|
|
|
(`meta.option_type`) listing every enabled type's key. Shown only when at least one
|
|
|
|
|
type is enabled.
|
|
|
|
|
- `Modules\Core\Product\Filament\Extensions\ValuesRelationManagerExtension` extends
|
|
|
|
|
the "Values" tab's form. Its `extendForm()` reads
|
|
|
|
|
`$option->meta['option_type']` off the owning `ProductOption`, resolves it via
|
|
|
|
|
`ProductOptionTypeManager`, and appends `getMetaForm()`'s fields to the stock name
|
|
|
|
|
field. A `ProductOption` with no type selected gets the stock form unchanged.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Reading the value back
|
|
|
|
|
|
|
|
|
|
Storefront code reads `ProductOptionValue::meta` like any other jsonb column — e.g.
|
|
|
|
|
`$value->meta['hex']` for a color swatch. `ProductOptionTypeManager` is an admin-side
|
|
|
|
|
concern only (describing *how to edit* the meta); nothing requires the storefront to
|
|
|
|
|
go through it to *read* the meta.
|