Looking to hire Laravel developers? Try LaraJobs

laravel-polymorphic-settings maintained by sitesource

Description
Polymorphic, per-model key-value settings for Laravel with opt-in encryption and transparent caching.
Author
Last update
2026/04/25 04:16 (dev-main)
License
Downloads
99

Comments
comments powered by Disqus

Polymorphic, per-model key-value settings for Laravel

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

Store settings that belong to any model — a team, a user, a tenant, or nothing at all — with opt-in encryption per key and transparent caching.

use SiteSource\PolymorphicSettings\Facades\PolymorphicSettings;

PolymorphicSettings::global()->put('site_title', 'Acme');
PolymorphicSettings::for($team)->put('theme', ['mode' => 'dark']);
PolymorphicSettings::for($user)->put('api_key', $secret, encrypted: true);

PolymorphicSettings::for($team)->get('theme.mode');   // 'dark'
$team->settings()->int('max_members', 25);            // typed getters

Why another settings package?

Most Laravel settings packages are class-based and global: you define a GeneralSettings class with typed properties, access app(GeneralSettings::class)->site_name. Great when you have a fixed schema and one set of values for your whole app.

This package is key-based and polymorphic: you attach settings to any Eloquent model at runtime. Per-team themes, per-user preferences, per-tenant feature flags, and plain old global settings all live in one table, all accessed through one fluent API.

Want… Reach for
Strongly-typed, class-based global settings spatie/laravel-settings
Runtime-scoped settings per team/user/tenant, plus encryption this package

Installation

composer require sitesource/laravel-polymorphic-settings

Run the installer — it detects your User model's primary key type, publishes the config, publishes the migration, and migrates. Non-interactive runs (CI, --no-interaction) use detected defaults:

php artisan polymorphic-settings:install

Or do each step manually:

php artisan vendor:publish --tag="polymorphic-settings-config"
php artisan vendor:publish --tag="polymorphic-settings-migrations"
php artisan migrate

Usage

Global settings

use SiteSource\PolymorphicSettings\Facades\PolymorphicSettings;

PolymorphicSettings::global()->put('site_title', 'Acme');
PolymorphicSettings::global()->get('site_title');   // 'Acme'

Scoped to any Eloquent model

PolymorphicSettings::for($team)->put('theme', 'dark');
PolymorphicSettings::for($team)->get('theme');      // 'dark'

// Same key, different scopes — fully isolated
PolymorphicSettings::for($userA)->put('locale', 'en-US');
PolymorphicSettings::for($userB)->put('locale', 'fr-FR');

Via the HasSettings trait

Clean up calling sites by adding the trait to your models:

use SiteSource\PolymorphicSettings\Concerns\HasSettings;

class Team extends Model
{
    use HasSettings;
}

$team->settings()->put('theme', 'dark');
$team->settings()->get('theme');          // 'dark'

// Eager load to avoid N+1
Team::with('scopedSettings')->get();

Opt-in cascade delete

Settings survive when their parent model is deleted by default. If you'd rather have them cleaned up:

class Team extends Model
{
    use HasSettings;

    public bool $cascadeDeleteSettings = true;
}

$team->delete();    // settings for this team are purged

SoftDeletes is respected — soft deletes don't cascade (so restore() works), only forceDelete() triggers the cleanup.

Encryption

Per-key, opt-in via a named argument:

$user->settings()->put('api_key', 'sk-live-abc', encrypted: true);
$user->settings()->get('api_key');        // 'sk-live-abc' (transparently decrypted)

Encryption is transparent — all(), getMany(), typed getters and dot-notation reads all see the plaintext. The raw DB value never contains the plaintext.

You can promote a plain setting to encrypted (and vice-versa) just by re-putting with a different encrypted: argument.

Bulk operations

$team->settings()->putMany([
    'theme' => 'dark',
    'locale' => 'en-US',
    'timezone' => 'UTC',
]);

$team->settings()->getMany(['theme', 'locale', 'missing']);
// ['theme' => 'dark', 'locale' => 'en-US', 'missing' => null]

$team->settings()->all();               // every setting in this scope
$team->settings()->forgetAll();         // nuke every setting in this scope

Dot-notation reads

$team->settings()->put('theme', [
    'palette' => ['primary' => ['hex' => '#abc123']],
]);

$team->settings()->get('theme.palette.primary.hex');   // '#abc123'

Typed getters

No implicit coercion — if the stored value is the wrong type, the default is returned:

$team->settings()->string('theme', 'default');
$team->settings()->int('max_members', 25);
$team->settings()->float('rate', 1.0);
$team->settings()->bool('feature_x', false);
$team->settings()->array('tags', []);

Miscellaneous

$team->settings()->has('theme');           // true
$team->settings()->forget('theme');         // bool — whether it existed
$team->settings()->pull('theme');           // get + forget atomically
$team->settings()->updateValues('theme', [  // merge into an existing array
    'accent' => '#f00',
]);

Configuration

config/polymorphic-settings.php (all values can also be set via env vars):

return [
    'table'    => env('POLYMORPHIC_SETTINGS_TABLE', 'polymorphic_settings'),
    'key_type' => env('POLYMORPHIC_SETTINGS_KEY_TYPE', 'int'),   // 'int' | 'uuid'

    'cache' => [
        'enabled' => env('POLYMORPHIC_SETTINGS_CACHE_ENABLED', true),
        'store'   => env('POLYMORPHIC_SETTINGS_CACHE_STORE'),    // null = default
        'ttl'     => env('POLYMORPHIC_SETTINGS_CACHE_TTL'),      // null = forever
        'prefix'  => env('POLYMORPHIC_SETTINGS_CACHE_PREFIX', 'polymorphic-settings'),
    ],
];

Caching

Reads are cached through Laravel's cache. put and forget invalidate automatically. Cache keys are scoped ({prefix}:{scope_key}:{setting_key}) so two different models can never collide.

Missing keys are intentionally not cached to sidestep cross-driver null-caching quirks. If you hammer get('nonexistent') in a hot path, either supply a default or populate the key.

UUID primary keys

The installer asks which PK type to use and auto-detects from your User model. If your app uses UUIDs:

POLYMORPHIC_SETTINGS_KEY_TYPE=uuid

The configurable_id column is always stored as a string, so int-keyed and UUID-keyed parent models can coexist in the same settings table regardless of this setting.

Using Settings as the facade alias

PolymorphicSettings is a mouthful. If you'd prefer Settings::for($team)->get(...):

// config/app.php
'aliases' => [
    'Settings' => SiteSource\PolymorphicSettings\Facades\PolymorphicSettings::class,
],

The default facade alias stays PolymorphicSettings to avoid collisions with any existing Settings class in your app.

Testing

composer test

The suite runs against SQLite, MySQL, and Postgres in CI (120 tests, ~194 assertions as of v0.1.0).

Changelog

See CHANGELOG.md.

Contributing

Please see CONTRIBUTING.md for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.