Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Polymorphic Settings Laravel Package

sitesource/laravel-polymorphic-settings

Laravel Polymorphic Settings lets you attach flexible key-value settings to any Eloquent model through a polymorphic relationship, with a simple API for storing, retrieving, updating, and managing configuration-like data per record.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require sitesource/laravel-polymorphic-settings
php artisan vendor:publish --provider="SiteSource\PolymorphicSettings\PolymorphicSettingsServiceProvider"
php artisan migrate

The publish step should give you a config file (config/polymorphic-settings.php) and a migration creating the settings table. Check both before running anything: the package is at 0 stars / single-digit adoption, so the migration's table name and the config's cache store key are things you want to confirm rather than assume.

Wire a model

Add the package's trait to any Eloquent model that needs settings:

use SiteSource\PolymorphicSettings\HasSettings; // verify namespace in src/

class Tenant extends Model
{
    use HasSettings;
}

Then the three public methods from the changelog are your whole API surface:

$tenant->put('commerce', ['checkout' => true, 'foo' => false]);
$tenant->get('commerce.foo');   // false
$tenant->forget('commerce');    // bool

get() is the one you'll call most; put() writes a whole value at a key; forget() drops it and always invalidates cache (since v0.1.3).

Where to look first

  1. config/polymorphic-settings.php — cache store, table name, encryption toggle.
  2. src/ — the trait, the cache-key builder (cacheKey()), and the value (de)serializer. Read these; they're small and they answer every question this README won't.
  3. CHANGELOG / release/v0.1.3.md — this is where the real behavioural contract lives.

First use case

Per-tenant feature toggles and UI preferences. It's a key-value store scoped to a model, so it maps naturally to "settings that belong to this tenant/user/team" and change without a deploy:

$tenant->put('notifications', ['digest' => 'weekly', 'mentions' => true]);

$digest = $tenant->get('notifications.digest', 'daily'); // default as 2nd arg

Prefer this over a JSON column when settings are edited one at a time from different admin screens and you want per-key cache invalidation rather than a full-row rewrite.

Implementation Patterns

Nest at the root, read at the leaf. Write a whole array under one root key and read children through dots. This keeps one cache entry per root and gets correctly busted by a single put('commerce', [...]):

$tenant->put('commerce', [
    'checkout'    => true,
    'currency'    => 'EUR',
    'tax'         => ['inclusive' => true],
]);

$tenant->get('commerce.tax.inclusive'); // true

Don't mix dotted and nested stores for the same key. put('a.b', x) and put('a', ['b' => x]) are distinct rows, and resolution is nested-first with a literal fallback. The v0.1.2 → v0.1.3 migration story exists precisely because someone had both. Pick nested-root writes as your house style and enforce it in review.

Wrap it in one settings object per model. You don't want get('key.name', ...) string literals scattered across controllers. A thin class gives you typed accessors and defaults in one place:

class TenantSettings
{
    public function __construct(private Tenant $tenant) {}

    public function digest(): string
    {
        return $this->tenant->get('notifications.digest', 'daily');
    }

    public function setDigest(string $value): void
    {
        // read-modify-write the root, not the leaf
        $current = $this->tenant->get('notifications', []);
        $this->tenant->put('notifications', [...$current, 'digest' => $value]);
    }
}

Encrypt only the leaves that need it. Encryption is opt-in, so treat it as a deliberate decision per key: API tokens, webhook secrets, SMTP passwords. Anything you want to WHERE-filter, sort, or report on in SQL must stay plaintext — encrypted values are opaque to the database.

Bulk-read the root when rendering a form. A page that renders 15 individual toggles via 15 get() calls is 15 cache hits at best and 15 queries on a cold cache. Read get('notifications', []) once and hand the array to the view.

Testing. Set the cache store to array in phpunit.xml, use RefreshDatabase, and flush the cache in setUp() — a stale entry from a previous test on the same morph/id pair is the classic false-green:

protected function setUp(): void
{
    parent::setUp();
    Cache::flush();
}

Filament/admin integration. There's a companion laravel-polymorphic-settings-filament package; the v0.1.3 release notes were driven by its integration, so that's the best-trodden admin path. If you're not using Filament, roll your own form over the TenantSettings wrapper above.

When you outgrow it. If you need history/audit of setting changes, per-key permissions, or queryable setting values, this package is the wrong shape — a dedicated settings table with typed columns is cheaper than bolting on an events layer.

Gotchas and Tips

Upgrade to v0.1.3 or disable the cache. Two real bugs are fixed only there. Pre-0.1.3, get('commerce.foo') cached the literal lookup under the dotted key, while put('commerce', [...]) only invalidated cacheKey('commerce') — leaving you serving a stale literal until TTL expiry. If you're pinned below 0.1.3 and can't move:

POLYMORPHIC_SETTINGS_CACHE_ENABLED=false

Settings reads aren't a hot path; per-request queries are fine as a stopgap.

Dotted-key literal reads are uncached by design (0.1.3+). The fix skips the cache for the literal fallback when the key contains a dot, so get('a.b') may cost a DB query every call. The nested root traversal path is still cached and still busted by put('root', ...). Don't build a hot loop over literal dotted keys.

forget() returns false on a successful cache bust. Since 0.1.3, forget() always invalidates cache but the boolean still reflects "did a DB row get deleted". Code like if (!$model->forget('x')) abort(404); will now spuriously 404 after an external deletion. Check existence separately if you care.

No merge semantics — concurrent root writes clobber. Read-modify-write on a root array is last-write-wins with no row locking. Two admins toggling different keys under notifications simultaneously will lose one change. Either keep admin surfaces single-key-per-form, or wrap the read-modify-write in a transaction with a lock on the settings row.

Morph aliases and class renames invalidate your cache silently. The cache key is built from morph type + id + key. Introducing Relation::enforceMorphMap() or renaming a model class doesn't migrate existing rows — you get orphaned settings and cold caches that look like "settings disappeared". Decide the morph map before you ship.

Hard-deleting a model leaves its settings behind. If the table's PK reuses the freed id, the new record inherits the old settings. Add a deleting hook (or a cleanup job) that calls forget() on the relevant roots — soft deletes keep settings intentionally, which is usually what you want.

Encryption key rotation is a one-way door. Values are decrypted through Laravel's encrypter; rotating APP_KEY without re-encrypting makes ciphertext unreadable, and the package exposes no re-key command in the docs. Either keep the settings encryption key out of routine rotation, or write your own decrypt-with-old-key / re-encrypt script before you rotate.

The cache store is shared infrastructure, not a detail. Use redis/memcached shared across web and queue workers, not array or file on ephemeral containers. If a queue worker writes a setting and the web tier's cache isn't invalidated, you get the 0.1.3 bug shape again — but self-inflicted.

Debugging checklist when a value "doesn't stick":

  1. POLYMORPHIC_SETTINGS_CACHE_ENABLED=false and retry — isolates cache vs. write.
  2. Dump the row directly (model_type, model_id, key) — check for a literal dotted row shadowing a nested one.
  3. Confirm the model is actually using the trait and that the morph alias matches existing rows.
  4. If the value is encrypted, verify get() is returning decrypted output at all (a wrong key throws, but a wrong config toggle can leave plaintext that later reads try to decrypt).

Pin the version and read the source. Zero stars, zero dependents, a very recent last release. It's a small, well-scoped package — a couple of hours reading src/ buys you a full mental model and the ability to fork if it goes unmaintained. That's a fine trade for a settings store, but budget for it before it's load-bearing across a dozen models.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect