3.9 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 enables a type class in config/core.php:
// config/core.php
'product_option_types' => [
\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.
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
— not auto-registered, since registration is always an explicit shop decision.
How it's wired into the admin UI
Modules\Core\Product\Services\ProductOptionTypeManager:
all(): Collection<string, ProductOptionTypeInterface>— every enabled type, keyed bygetKey().resolve(?string $key): ?ProductOptionTypeInterface— looks up one by key (ornullif no key / not found).
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.