konekt/gears
Konekt Gears adds feature flags to Laravel: define gears in code or database, toggle them globally or per model, and schedule time-based activation. Blade directives, middleware and Eloquent scopes let you gate features without redeploying.
Install and wire up
composer require konekt/gears
php artisan vendor:publish --provider="Konekt\Gears\Providers\GearsServiceProvider"
php artisan migrate
Publishing gives you config/gears.php (backend selection, cache, table name) and the migration that creates the gears table. The table is a single generic store — one row per key per scope — so you do not add a column every time you add a setting.
Where to look first
config/gears.php — confirms your backend (DatabaseBackend by default) and optional cache.AppServiceProvider::boot() — this is where you register every setting and preference your app knows about (see below). Registration is what makes a key "known" and gives it a default; it's the closest thing this package has to a schema.HasPreferences trait — drop it onto your User model.First real use case: a site-wide knob + a per-user toggle
In AppServiceProvider::boot():
use Konekt\Gears\Facades\Settings;
use Konekt\Gears\Facades\Preferences;
Settings::register('site.tagline', 'Built with Laravel');
Preferences::register('notifications.weekly_digest', true);
Then anywhere in the app:
echo Settings::get('site.tagline'); // falls back to the registered default
Preferences::set('notifications.weekly_digest', false, $user);
$user->preferences->get('notifications.weekly_digest'); // false
That's the whole mental model: registration defines the key and its default; get/set/forget operate on it; preferences are the same but scoped to a model instance.
Centralise registration in a dedicated provider
Don't scatter Settings::register() across feature providers — you'll lose track of what keys exist. A single SettingsServiceProvider (or a config/gears-registry.php file iterated in boot()) that lists every key and default is far easier to audit and is what you'll grep for six months later.
// app/Providers/GearRegistryServiceProvider.php
public function boot(): void
{
Settings::register('billing.currency', 'USD');
Settings::register('features.beta_ui', false);
Settings::register('mail.from', config('mail.from.address'));
Preferences::register('ui.theme', 'light');
Preferences::register('ui.rows_per_page', 25);
Preferences::register('email.marketing_opt_in', true);
}
Settings page pattern
For an admin settings screen, read the whole group, validate, then persist. Because values are loosely typed by design, cast on the way out and normalise on the way in:
// Controller
$currency = Settings::get('billing.currency');
$betaUi = (bool) Settings::get('features.beta_ui');
// Save
Settings::set('billing.currency', $request->validated('currency'));
Settings::set('features.beta_ui', $request->boolean('beta_ui'));
For preferences, resolve the model once and hit the trait:
$user->preferences->set('ui.rows_per_page', (int) $request->input('rows'));
Reading in Blade / views
Settings are cheap reads (especially with the cache backend on), so calling Settings::get() directly in a view is acceptable. For preferences in a Blade layout, resolve from the authenticated user:
<html data-theme="{{ auth()->user()?->preferences->get('ui.theme') ?? 'light' }}">
Backend swapping for tests
The pluggable backend is the most useful extension point day-to-day. Point the config at the in-memory backend in phpunit.xml or a test service provider so tests stay isolated:
<env name="GEARS_BACKEND" value="memory"/>
Then each test starts with a clean slate and no DB round-trips. Check the config file for the exact env key — the backend is resolved from config('gears.backend').
Prefixing by domain
There's no built-in namespace/group concept beyond what you put in the key string. Adopt a convention early — billing.currency, ui.theme, mail.from — and stick to it. You'll want to bulk-export or clear a domain later, and a prefix is the only handle you'll have.
Caching wrapper
If you enable the cache layer in config, reads hit your cache store and writes invalidate automatically. This is the standard reason to enable it — settings are read far more than written, and Settings::get() in a hot path otherwise means an extra query per request.
Unregistered keys still work, which is a foot-gun. Settings::get('foo') on a key you never registered will typically return null rather than throwing. That means a typo silently reads as "off" or an empty string. Two defences: keep registration mandatory in code review, and if you need to distinguish "never set" from "set to null", check the raw backend rather than the facade. If the package exposes an existence check (e.g. has), use it in validation rules for any key coming from user input.
Preferences need the trait and a saved model. $user->preferences requires HasPreferences on the model and the model to have an ID — preferences are stored polymorphically against model_type/model_id, so an unsaved model has nowhere to write. Guard against auth()->user() being null in shared layouts.
Values are not strongly typed in storage. Booleans, integers, and nulls make a round trip through the generic store. Always cast explicit on read ((bool), (int)), and be aware (bool) "false" is true if something stringified on the way in. Normalise at write time.
Cache invalidation is per-key, not per-request. If you enabled caching, remember that a Settings::set() in one request invalidates the cached value for subsequent requests, not for the current in-request read (depending on implementation). Don't read a setting you just wrote and assume it's fresh in the same request — use the value you passed to set() instead. In tests, the cache layer is a common source of "flaky" behaviour; disable it or use the memory backend.
The single gears table is global; multi-tenancy is DIY. There is no tenant column built in. If you need per-tenant settings you either prefix keys (tenant.{id}.foo) or extend the backend. The latter is cleaner but means implementing the backend interface yourself — do the prefixing until you genuinely outgrow it.
Extending via a custom backend is the sanctioned extension point: implement the Backend contract (get/set/forget/all) and swap the class in config/gears.php. This is the right move if you want settings in Redis, in an external config service, or tenant-scoped. Prefer this over reaching into the DB table directly.
Debugging: start by inspecting the gears table directly — select * from gears where key = '...' tells you instantly whether the problem is storage or registration. If a value "doesn't stick", it's usually (a) unregistered key, (b) writing to the wrong scope (settings vs preferences), or (c) cache. If a preference is "shared between users", you almost certainly forgot to pass the model on set().
Facades vs injection. The Settings/Preferences facades are convenient but make testing and typing muddier. For application services, inject the underlying Gears service (or bind an interface to it) so you can swap the backend in tests without touching the container config.
Register defaults once, not on every boot with dynamic values. Settings::register('x', now()) bakes a timestamp into the "default" every deploy. Keep defaults static; use the storage value for anything that must change.
Watch the migration. If you already have a gears table from another package or a previous install, the published migration may conflict. Rename via config/gears.php before migrating, not after.
How can I help you explore Laravel packages today?