Skip to main content

← Eager Loading · Back to README · Payload Casts →

Configuration

Publish the configuration​

php artisan vendor:publish --tag="model_settings"

This publishes config/model_settings.php and the package migration.

Available options​

OptionDefaultPurpose
modelDragonCode\LaravelModelSettings\Models\SettingsEloquent model used for stored settings
connectionApplication defaultDatabase connection used by the model and migration
tablesettingsDatabase table used by the model and migration
casts[]Payload casts selected by parent model class and optionally setting key

The package reads these environment variables:

VariableDefault
MODEL_SETTINGS_DATABASE_CONNECTIONDATABASE_CONNECTION, then Laravel's default connection
MODEL_SETTINGS_DATABASE_TABLEsettings

Set the connection and table before running the migration:

MODEL_SETTINGS_DATABASE_CONNECTION=mysql
MODEL_SETTINGS_DATABASE_TABLE=model_settings

Changing either value later does not move existing records.

Payload cast configuration​

The legacy model-wide form remains supported. One cast handles every payload owned by the model class:

'casts' => [
App\Models\User::class => App\Casts\UserSettingsPayloadCast::class,
],

Use a key-aware map when different keys need different types or handling:

'casts' => [
App\Models\User::class => [
'profile' => App\Data\ProfileData::class,
'billing.credentials' => App\Casts\EncryptedSettingPayload::class,
],
],

Key matching is exact. Dots have no nested-path meaning, and a key missing from the map uses the default JSON cast. Each model entry is either a model-wide class string or a key-aware map; there is no wildcard entry inside a key-aware map. See Payload Casts for supported cast contracts and an encryption recipe.

Storage schema​

The published migration creates these columns:

ColumnPurpose
idSettings row primary key
item_typeParent model morph class or alias
item_idParent identifier, stored as a string up to 36 characters
is_defaultDistinguishes class defaults from model overrides
keySetting key
payloadPayload declared by the migration as jsonb
created_at and updated_atLaravel timestamps

The combination of item_type, item_id, is_default, and key is unique. A lookup index on item_type, is_default, and item_id supports default and owner-scope reads.

Class defaults and model overrides share this table. The package does not create a second defaults table or add encryption metadata columns.

The default item_id column stores at most 36 characters. Integer, string, UUID, and ULID identifiers fit this schema when their string form is no longer than 36 characters. A longer custom primary key requires a matching migration change.

Class defaults use item_id = '0' with is_default = true. A persisted owner whose key is integer 0 or string '0' uses the same physical item_id with is_default = false, so both rows can coexist for the same model type and setting key. Changing the database connection, table name, or morph-map aliases after data exists requires moving or updating the existing rows yourself.

Upgrade from an earlier 1.x release​

This release changes the runtime contract in addition to the storage discriminator migration:

Earlier 1.x behaviorCurrent behaviorRequired application change
set($key, null), empty strings, whitespace strings, and empty arrays deleted the rowset() stores every JSON value exactlyReplace deletion calls with forget($key)
Blank entries in setMany() were deleted in the same batchEvery setMany() entry is stored in one transactional upsertMove deleted keys to a separate forgetMany() call
Empty and whitespace-only keys were acceptedNormalized blank keys throw InvalidSettingKeyRename or remove invalid keys before upgrading
Existence checks required all()->has($key)has($key) distinguishes stored JSON null from a missing keyPrefer the focused has() method

A stored null model override now hides a filled class default until forget() removes the override. Custom payload casts now receive blank values from both setters, and custom storage-model create or update events receive them from set(). get() still accepts only one argument; no caller fallback or permanent put() alias was added.

After updating the package, publish its new migration and run it with the application in maintenance mode:

php artisan vendor:publish --tag="model_settings"
php artisan migrate

The upgrade migration adds is_default, classifies every legacy row with item_id = '0' as a class default, creates the discriminator-aware indexes, and then removes the legacy unique index. It never writes setting keys or payloads to migration output.

Earlier 1.x schemas encoded class defaults and real owner ID 0 rows identically. The migration therefore cannot distinguish a manually inserted owner override from a default and classifies both as defaults. Inspect any known legacy owner-ID-0 data after migration and set is_default = false for rows that are actual model overrides.

Do not run the old package runtime against the upgraded schema. It does not write the discriminator and would store defaults as overrides. Deploy the migration and compatible runtime as one maintenance boundary.

Rollback is safe only before a real owner-ID-0 override exists. The migration stops before changing the schema when it finds item_id = '0' with is_default = false, because the legacy schema cannot represent that row without changing its meaning. Remove or export those overrides before rolling back. A safe rollback restores the legacy unique index and removes is_default.

Replace the storage model​

The built-in settings model is final. Configure a replacement instead of extending it:

namespace App\Models;

use DragonCode\LaravelModelSettings\Casts\PayloadCast;
use Illuminate\Database\Eloquent\Model;

final class ApplicationSetting extends Model
{
protected $fillable = [
'item_type',
'item_id',
'is_default',
'key',
'payload',
];

public function __construct(array $attributes = [])
{
$this->setConnection(config('model_settings.connection'));
$this->setTable(config('model_settings.table'));

parent::__construct($attributes);
}

protected function casts(): array
{
return [
'item_id' => 'string',
'is_default' => 'boolean',
'payload' => PayloadCast::class,
];
}
}

Then update the config:

'model' => App\Models\ApplicationSetting::class,

The replacement must remain compatible with the published schema. Keep the fillable attributes and the PayloadCast unless the replacement implements equivalent serialization.

At minimum, the replacement model must preserve these behaviors:

RequirementReason
Fill item_type, item_id, is_default, key, and payloadStorage writes these attributes
Use the configured connection and tableThe migration and repository must address the same rows
Cast item_id to stringInteger, string, UUID, and ULID identifiers share one column
Cast is_default to booleanLazy and eager resolution must read the same scope discriminator
Cast payload with PayloadCast or an equivalentReads and writes must preserve JSON behavior

See Also​