4.4 KiB
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
A shop registers a type class from its own service provider's boot(), the same
shape as Modules\Core\Notification\NotificationRegistry:
use Modules\Core\Product\Services\ProductOptionTypeManager;
ProductOptionTypeManager::get()->register([
\App\ProductOptions\ColorOptionType::class,
]);
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.
Writing a type
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.
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 is a singleton registry:
get(): static— the shared instance.register(array $types): void— registers one or more type classes, keyed internally bygetKey().unregister(string $key): voidresolve(?string $key): ?ProductOptionTypeInterface— looks up a registered type by key (ornullif no key / not found).all(): array<string, class-string>— every registered type's class, keyed bygetKey().
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\ProductOptionResourceExtensionextendsLunar\Admin\Filament\Resources\ProductOptionResource's own form with aSelect(meta.option_type) listing every enabled type's key. Shown only when at least one type is enabled.Modules\Core\Product\Filament\Extensions\ValuesRelationManagerExtensionextends the "Values" tab's form. ItsextendForm()reads$option->meta['option_type']off the owningProductOption, resolves it viaProductOptionTypeManager, and appendsgetMetaForm()'s fields to the stock name field. AProductOptionwith 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.