orchid/settings
Orchid Settings is a Laravel package for the Orchid Platform that provides an admin settings screen for managing application configuration values without code changes. It offers a simple API for storing and retrieving settings, making runtime configuration easy to maintain.
Install, publish, migrate.
composer require orchid/settings
php artisan vendor:publish --provider="Orchid\Settings\SettingsServiceProvider"
php artisan migrate
Publishing gives you config/settings.php (cache store/TTL and model binding) and the settings migration. Run it against a real database — everything lives in one key/value table behind an Eloquent model. There is no schema to design beyond the key convention you choose.
Where to look, in order.
config/settings.php — cache driver, TTL, and the model class. Coherence behaviour is decided here; read it before writing a line of code.group column exists) so you know what "groups" actually mean in storage.Orchid\Settings\Facades\Settings and the global setting() helper. These are the whole surface. Everything else is internal and should not be depended on.First use case — prove the loop. Pick one cosmetic, global value (site tagline, support email) and validate read → write → read → invalidation across processes:
// Read with an application-owned fallback
$tagline = setting('site.tagline', config('app.name'));
// Write
setting(['site.tagline' => 'Built for operators']);
// Facade equivalent
use Orchid\Settings\Facades\Settings;
Settings::set('site.tagline', 'Built for operators');
Settings::get('site.tagline', 'fallback');
If the round-trip is fresh in a second process, you understand the package. If it isn't, stop and fix cache configuration before building anything on it.
Framework gate. 5.0.0 exists to add Laravel 9 support — that is the entire changelog. Check the tag's real constraints with composer why-not orchid/settings 5.0.0 rather than trusting the release note. On Laravel 8 or older, verify 4.x is still viable or that 5.x hasn't raised the floor past you.
Always read through a default. The second argument is your application-owned fallback and the thing that makes the dependency replaceable. Never call setting('key') bare in a path that matters:
$from = setting('mail.from.address', config('mail.from.address'));
Writes are configuration events, not request state. Write from the admin screen, a seeder, or a deploy hook — never on a controller hot path. Batch in the array form so you pay a single invalidation:
setting([
'features.new_checkout' => true,
'ui.banner.text' => 'Maintenance Sunday 02:00 UTC',
]);
Keep a key registry. There is no schema, so without discipline the table becomes a dumping ground and nobody knows which keys are live. Put keys in constants (or an enum) and document owner, type, and default per key:
final class SettingKey
{
public const SITE_TAGLINE = 'site.tagline';
public const FEATURE_CHECKOUT = 'features.new_checkout';
}
Use dot notation as your grouping convention. Whether or not the package's "groups" are first-class in storage, mapping stable prefixes (mail.*, features.*, ui.*) is what makes the Orchid screen navigable and all()-style dumps readable.
Wrap it in one app-level service. Highest-leverage decision you'll make. All reads and writes funnel through a thin class that delegates to the facade; if the package goes stale you swap one file:
final class AppSettings
{
public function get(string $key, mixed $default = null): mixed
{
return cache()->rememberForever(
"settings.{$key}",
fn () => Settings::get($key, $default)
);
}
public function set(string $key, mixed $value): void
{
Settings::set($key, $value);
cache()->forget("settings.{$key}");
}
}
The layered cache is defensive, not redundant: a package upgrade that changes internal cache-key format won't silently change your coherence behaviour.
Cast at the boundary, not at every call site. Values arriving from an HTML form are strings, and "false" is truthy. Normalise types inside AppSettings, then read toggles with an explicit boolean default so a missing key fails closed deliberately:
if (setting(SettingKey::FEATURE_CHECKOUT, false) === true) { ... }
Seeders and imports go through the API. DB::table('settings')->insert(...) is the classic cause of "why is prod showing the old value" — it bypasses invalidation. Use Settings::set() in seeders too. For bulk imports, follow with a cache flush.
Orchid integration is the business case. Register the published settings screen so operators edit values directly. Keep your own validation layer around it — the package gives CRUD, not rules, not an audit trail. If the screen is your only write path, you get approval-by-process rather than approval-by-code, which is often fine.
Testing. RefreshDatabase won't clear the cache. Point the cache at the array driver in phpunit.xml or flush settings cache in setUp(). Write exactly one coherence test — write in one call, read through your service in another, assert freshness. That test catches upgrade regressions better than anything else you can write.
5.0.0 is a major bump with a one-line changelog. "Support Laravel 9" says nothing about storage format, config keys, helper signatures, or cache-key format. Read the 4.0 → 5.0.0 diff before upgrading, pin the version explicitly in composer.json so it can't arrive via an unrelated composer update, and treat the upgrade as breaking by default.
Global scope only. No per-tenant or per-user scoping. Prefixing keys (tenant.42.feature) works but is an application design built on top, not a package feature — and it makes all() and the admin screen useless quickly. If you need multi-tenancy, this is the wrong tool.
Direct DB writes silently break coherence. Any admin SQL, seeder, or import bypassing the API leaves nodes serving stale values until TTL expiry or a manual flush. Grep for DB::table('settings') and make "no direct table access" an operator rule, not tribal knowledge.
Multi-node staleness. A shared cache (Redis/Memcached) propagates invalidation; per-node file/array caches do not. Confirm production uses a shared store before trusting writes to be visible everywhere, and add a deploy-hook php artisan cache:clear if you're not certain.
Helper name collision. setting() is a common name in older codebases. Check for an existing declaration first; if one exists, use the Settings facade exclusively rather than fighting function redeclaration.
Cache makes reads cheap and writes expensive. Read-heavy, write-light is the design. Hundreds of keys, not thousands. If you're writing settings per-request or per-tenant, invalidation cost will dominate and you've picked the wrong storage.
No validation, no audit, no approval workflow. These are yours. At minimum, add an append-only change log written by your set() wrapper so you can answer "who changed this and when" during an incident.
Don't gate entitlements, pricing, or access control on it. Single maintainer, near-zero adoption signal, no SLA. A settings table with an application-owned fallback is cheap and replaceable; a billing check depending on an unmaintained package is not. Keep anything business-critical reading through your service with a hardcoded or config fallback.
Review the migration on every upgrade. Added columns are fine; an up() that alters or truncates the settings table needs a backup and a dry run against production-shaped data.
Keep the integration thin. Facade and helper only — no direct model queries, no direct table access. That single rule is what makes this a one-file swap if the maintainer goes quiet.
How can I help you explore Laravel packages today?