NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Options for Laravel

Database schema & key type

One table, options, stores every value — global and owned:

Schema::create('options', function (Blueprint $table) use ($keyType): void {
    $table->id();
    $table->morphKey('owner', $keyType, nullable: true); // owner_type + owner_id
    // 'global' or a hash of owner type + id, filled by the model
    $table->string('owner_scope', 32);
    $table->string('key')->index();
    $table->text('value')->nullable();
    $table->jsonb('meta')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->index(['owner_type', 'owner_id', 'key']);
    $table->unique(['owner_scope', 'key']);
});
ColumnTypeNotes
idbigint, auto-incrementPrimary key.
owner_type / owner_idnullable morph (per key_type)Both null = a global value; otherwise the owning model.
owner_scopestring(32)global, or a hash of owner type + id. The model fills it on every save; unique together with key.
keystring, indexedThe option’s key().
valuetext, nullableThe raw stored string — the value serialized through its cast, and encrypted for encrypted options. Cast back on every read.
metajsonb, nullableFree-form metadata, cast to a Collection on the model.
created_at / updated_attimestamps—
deleted_atsoft deletesforget() soft-deletes the row.

A row with a null owner_type and owner_id is a global value; any other row belongs to the model it morphs to. The composite index on (owner_type, owner_id, key) makes a scoped lookup a single indexed query.

The unique index on (owner_scope, key) holds the table to one row per option and scope. Two requests writing a new option at the same moment can’t store it twice — the second write updates the first one’s row — and a forgotten option’s soft-deleted row is reused by the next set() or import.

Owner key type

key_type types the polymorphic owner columns. It describes the models that own values — never the options table itself, which always keeps an auto-incrementing id:

key_typeOwner columnsUse when your owners…
bigintnullableMorphsuse Laravel’s default auto-incrementing keys (the default)
uuidnullableUuidMorphsuse HasUuids
ulidnullableUlidMorphsuse HasUlids

It’s read when the migration runs, so set it first. The value is case-insensitive; anything other than bigint, uuid or ulid throws InvalidConfigurationException when the migration runs instead of falling back.

The Option model

RoundlyConsulting\Options\Option is a regular Eloquent model with soft deletes, a factory and an owner() morphTo relation. The value column has no static cast — each option applies its own castAs() at read and write time. Useful members:

use RoundlyConsulting\Options\Option;

Option::query()->forOwner($user)->get();   // rows owned by $user
Option::query()->forOwner(null)->get();    // global rows only

$row = Option::query()->forOwner($user)->where('key', 'theme')->first();

$row->owner;         // the owning model (null for a global row)
$row->owner_scope;   // 'global', or a hash of owner type + id (set on every save)
$row->meta;          // Collection|null

Custom model

Point options.model at your own subclass to add relations, scopes or accessors — every read, write, export and import goes through it:

namespace App\Models;

use RoundlyConsulting\Options\Option;

class TenantOption extends Option
{
    // extra relations, scopes or accessors
}

// config/options.php
'model' => App\Models\TenantOption::class,

A key that is not set — absent, null or blank — resolves the packaged model. Anything else must be Option or a subclass of it, or the toolkit’s InvalidConfigurationException is thrown naming the key — a foreign class is never silently replaced with the packaged model.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.