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 Options Laravel Package

appstract/laravel-options

Store and retrieve global key-value options in Laravel with a simple option() helper and Options facade. Values persist in a database table, support caching for fast access, and can be easily typed, making site-wide settings manageable without touching config files.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes):

composer require appstract/laravel-options
php artisan vendor:publish --provider="Appstract\Options\OptionsServiceProvider" --tag="migrations"
php artisan migrate

This creates an options table (key / value) and registers an Option facade. No config file is strictly required for basic use — the package is intentionally thin.

Where to look first:

  1. The published migration — confirm the key column is unique and check the value column type/length. That single decision (usually TEXT or VARCHAR(255)) is the biggest constraint you'll hit later.
  2. vendor/appstract/laravel-options/src/ — the whole package is a handful of files (model + service provider + facade). Read it in one sitting; you'll understand every behaviour, including whether values are cached.
  3. Your config/app.php aliases if you're on an older Laravel skeleton — the facade may need registering manually.

First use case: site-wide toggles that don't deserve their own table — maintenance_mode, support_email, footer_tagline, feature.new_checkout. Anything a non-developer might want to change without a deploy, but that doesn't warrant a full settings UI package.

use Appstract\Options\Option;

Option::set('support_email', '[email protected]');
Option::get('support_email', '[email protected]'); // second arg = default
Option::has('maintenance_mode');
Option::remove('maintenance_mode');

Implementation Patterns

Wrap it. Always. Don't sprinkle Option::get() through controllers and Blade. The package gives you a global mutable store with no typing and no schema — that's a footgun at scale. Put a thin typed layer in front of it:

final class Settings
{
    public static function maintenanceMode(): bool
    {
        return (bool) Option::get('maintenance_mode', false);
    }

    public static function supportEmail(): string
    {
        return Option::get('support_email', config('mail.from.address'));
    }
}

Now the key strings live in one place, defaults are explicit, and casts are consistent. When you later migrate to spatie/laravel-settings or a config-driven approach, you only touch this class.

Store structured data as JSON, deliberately.

Option::set('social_links', json_encode(['twitter' => '...', 'github' => '...']));
$links = json_decode(Option::get('social_links', '{}'), true);

Keep the serialization in your wrapper, not at call sites. Never let two different call sites disagree about whether a key holds JSON or a scalar.

Admin settings form. The natural integration is a resourceful controller + Blade form that loops a whitelisted array of keys. Critically, whitelist the keys — never Option::set($request->input('key'), ...) directly. This table is global and unauthenticated writes are a privilege-escalation vector if you expose it.

$allowed = ['support_email', 'footer_tagline', 'maintenance_mode'];
foreach ($allowed as $key) {
    if ($request->has($key)) {
        Option::set($key, $request->input($key));
    }
}

Caching. If the package doesn't cache internally (verify in the source — for a package this small, likely it does a query per get), put a cache in front from day one:

public static function get(string $key, $default = null)
{
    return Cache::rememberForever("option.{$key}", fn () => Option::get($key, $default));
}

…and forget the key on every set/remove in a matching put() helper. This converts a per-request query into a per-deploy one. Note the cache layer belongs in your wrapper, not scattered around — otherwise invalidation will drift.

Middleware for the toggle use case. A maintenance_mode middleware reading Settings::maintenanceMode() is the canonical "this package earned its place" integration:

public function handle($request, Closure $next)
{
    if (Settings::maintenanceMode() && ! $request->user()?->isAdmin()) {
        abort(503);
    }
    return $next($request);
}

Testing. With RefreshDatabase the table is truncated between tests, which is usually what you want. For tests that depend on settings, seed them in setUp() or a factory rather than relying on production values. If you cache in the wrapper, call Cache::flush() in tearDown or use the array cache driver — stale cache across tests is a classic afternoon-waster.

Seeding. Use Option::set() in seeders rather than raw DB inserts so your wrapper's cache-invalidation logic runs. Use updateOrCreate-style semantics inside set() if the package doesn't already do it — check the source; if set() is a plain create(), the second call on the same key will throw or duplicate depending on your unique index.

Gotchas and Tips

The value column is your ceiling. Many of these micro-packages ship VARCHAR(255). Any JSON blob, HTML snippet, or long text will silently truncate or throw (depending on MySQL strict mode). Check the published migration before you commit to storing anything non-trivial, and change it to TEXT in your own migration if needed — you can edit the published file, it's yours now.

No typing, no validation, no schema. Everything is a string. Option::get('maintenance_mode') returns "1" / "0" / null, not true. Cast religiously in your wrapper. (bool) "0" is false, but (bool) "false" is true — a real hazard if anyone ever writes the literal string "false" into the table. Prefer "1"/"0" conventions and enforce them in the admin form.

Don't store secrets. Values are plaintext in the database, visible to anyone with DB access or a php artisan db:table command, likely dumped into backups in the clear, and there's no encryption hook. API keys and tokens belong in .env / encrypted casts on a real model — not here.

Multi-tenancy is not supported. This is a global store, exactly as advertised. If you have tenants, key collides immediately. You'd need to either add a tenant_id column and scope every query (at which point you should ask why you're not using per-tenant settings rows on the tenants table), or run separate databases per tenant.

Cache invalidation across servers. If you add Cache::rememberForever as suggested and run multiple app servers on the file/array driver, a set() on one node won't invalidate the others. Use Redis/Memcached (shared) or Cache::tags() where supported. This is the single most likely production bug in a naive rollout.

Concurrency. Two admins saving different keys simultaneously is fine; the same key is last-write-wins. There's no optimistic locking. That's acceptable for a settings table — just don't use this store for counters or anything where lost updates matter.

Key naming discipline. Use a dotted namespace (mail.support_address, feature.checkout_v2) from the start. Flat keys like email become ambiguous within months, and since there's no schema you have no way to rename them safely later except a hand-written migration.

Debugging stale values. If Option::get() returns something you know you changed, the culprit is almost always a cache layer (yours or the package's). Confirm with DB::table('options')->where('key', ...)->first() and Cache::forget(...) before touching application code.

Extension points are limited but real. The model is a plain Eloquent model — you can subclass it, add accessors/mutators (e.g. auto-JSON-casting value based on a key suffix), or replace it via a config binding if the package supports one. Check the service provider for a config('options.model') hook; if it's absent, a facade swap in your own service provider is the cleanest override. Don't fork the package for this — 728 stars and a tiny surface area mean it's stable, but a fork will rot.

Version pinning and maintenance. Last release is dated recently and the API is intentionally minimal, so upgrade risk is low — but pin ^1.0 (or whatever your install resolves to) in composer.json and note the package in your dependency audit. Small packages with a single maintainer are exactly the ones that go quiet; the fact that your entire integration is behind a Settings class means a future replacement is a one-file change.

When to graduate. If you find yourself adding typed casting, validation, caching, multi-tenant scoping, and an admin UI on top of this package, you've rebuilt spatie/laravel-settings badly. Recognize that inflection point and switch — the wrapper class you wrote makes the migration mechanical, which is precisely why you wrote it.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core