Files
core/docs/product-options.md
T

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\Catalog\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\Catalog\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\Catalog\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\Catalog\OptionTypes\ColorOptionType, registered automatically by Modules\Core\Providers\CatalogServiceProvider — 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\Catalog\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 classes needed:

  • Modules\Core\Catalog\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\Catalog\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.