metalinked / laravel-settings-kit
A Laravel package for managing global and user-specific settings with multilingual support, built-in caching, and a full REST API.
Package info
github.com/metalinked/laravel-settings-kit
pkg:composer/metalinked/laravel-settings-kit
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
- laravel/framework: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- mockery/mockery: ^1.4.4
- nunomaduro/collision: ^6.0|^7.0|^8.0|^9.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^9.5|^10.0|^11.0
README
A Laravel package for managing global and user-specific settings with multilingual support, built-in caching, and a full REST API.
Features
- Two-level settings. Global defaults with per-user overrides. Users who haven't customised a setting automatically inherit the global default, with no extra queries.
- Type casting. Values are stored as strings and automatically cast to
boolean,integer,json,select, orstringon retrieval. - Built-in cache. Every read is cached. Changing a global default is immediately visible to all users who haven't customised it.
- Batch operations. Update multiple settings in a single call. Retrieve all settings for a user, or only the ones they've personalised.
- Multilingual labels. Attach translated titles and descriptions to settings, with locale fallback.
- Role-based visibility. Tag settings with a role to control which users see them.
- REST API. Full API for SPAs, mobile apps, and headless applications, with token, Sanctum, and Passport authentication.
- Events. A
SettingUpdatedevent is dispatched on every write and reset, for audit logs and side effects. - Blade directive.
@setting('key')renders a setting value directly in templates. - Artisan commands.
settings:list,settings:export,settings:import,settings:clear-cache.
Requirements
PHP 8.1 or higher, Laravel 9 or higher.
Installation
composer require metalinked/laravel-settings-kit php artisan vendor:publish --provider="Metalinked\LaravelSettingsKit\SettingsKitServiceProvider" --tag="migrations" php artisan migrate
Publish the config file (optional):
php artisan vendor:publish --provider="Metalinked\LaravelSettingsKit\SettingsKitServiceProvider" --tag="config"
Basic Usage
use Metalinked\LaravelSettingsKit\Facades\Settings; // Read a global setting $value = Settings::get('site_name'); // Read a user-specific setting (falls back to the global default if not customised) $theme = Settings::get('theme', $userId); // Write a global setting (the preference must exist in the database) Settings::set('site_name', 'My App'); // Write a user-specific setting Settings::set('theme', 'dark', $userId); // Write and auto-create the preference if it does not exist yet Settings::setWithAutoCreate('new_feature_enabled', true); // Get or auto-create with a default value $perPage = Settings::remember('items_per_page', 20); // Check a boolean setting if (Settings::isEnabled('maintenance_mode')) { abort(503); } // Read multiple settings in a single query $settings = Settings::getMultiple(['theme', 'language', 'timezone'], $userId); // Update multiple settings in a single call Settings::setMultiple(['theme' => 'dark', 'language' => 'ca'], $userId); // Reset a user's customisation, reverting to the global default Settings::forget('theme', $userId); // Reset all of a user's customisations at once $count = Settings::forgetAll($userId); // Permanently delete a preference and all its user customisations Settings::delete('old_feature_flag');
Auto-creation and type detection
setWithAutoCreate() creates the preference automatically if it does not exist, detecting the type from the PHP value:
Settings::setWithAutoCreate('maintenance_mode', false); // boolean Settings::setWithAutoCreate('max_users', 100); // integer Settings::setWithAutoCreate('theme_config', ['dark' => true]); // json Settings::setWithAutoCreate('site_name', 'My App'); // string
Predefined settings (recommended)
For production use, create settings upfront via a seeder so you have full control over type, defaults, and constraints:
use Metalinked\LaravelSettingsKit\Models\Preference; Preference::firstOrCreate(['key' => 'maintenance_mode'], [ 'type' => 'boolean', 'default_value' => '0', 'category' => 'system', 'is_user_customizable' => false, ]); Preference::firstOrCreate(['key' => 'theme'], [ 'type' => 'select', 'default_value' => 'light', 'options' => ['light', 'dark', 'auto'], 'category' => 'appearance', 'is_user_customizable' => true, ]);
Values for select settings are validated on write: passing a value not in options throws an InvalidArgumentException.
Global vs. User Settings
The package distinguishes two types of settings based on the is_user_customizable column.
Global unique settings (is_user_customizable = false) apply to the entire application. Calling Settings::set('maintenance_mode', true) updates default_value directly and affects everyone immediately.
User-customisable settings (is_user_customizable = true) have a global default that individual users can override. The global default lives in preferences.default_value. Per-user overrides are stored in user_preferences only when the user actually changes the value, keeping the table lean.
// Change the global default (affects all users who have not customised) Settings::set('theme', 'dark'); // User 123 sets a personal preference (stored in user_preferences) Settings::set('theme', 'custom', 123); Settings::get('theme'); // 'dark' (global default) Settings::get('theme', 123); // 'custom' (user override) Settings::get('theme', 456); // 'dark' (inherits global default)
Loading all settings for a user
allForUser() is designed for app boot: one query fetches all customisable settings with resolved values and an is_overridden flag showing whether the user has personalised each one.
$settings = Settings::allForUser($userId, locale: 'ca'); // [ // 'theme' => ['value' => 'dark', 'is_overridden' => true, 'type' => 'select', ...], // 'language' => ['value' => 'en', 'is_overridden' => false, 'type' => 'string', ...], // ]
To retrieve only the settings a user has explicitly overridden:
$overrides = Settings::getUserOverrides($userId); // ['theme' => 'dark'] (only the keys with a personal override)
Multilingual Labels
Settings::createWithTranslations('notification_email', [ 'type' => 'boolean', 'default_value' => '1', 'category' => 'notifications', ], [ 'en' => ['title' => 'Email Notifications', 'text' => 'Receive notifications via email'], 'es' => ['title' => 'Notificaciones por Email', 'text' => 'Recibir notificaciones por correo'], 'ca' => ['title' => 'Notificacions per Email', 'text' => 'Rebre notificacions per correu'], ]); // Retrieve translated labels (falls back to the configured fallback_locale if missing) Settings::label('notification_email', 'ca'); // 'Notificacions per Email' Settings::description('notification_email', 'fr'); // fallback to 'en' // Add translations to an existing setting Settings::addTranslations('theme', [ 'en' => ['title' => 'Theme', 'text' => 'Choose your preferred theme'], 'es' => ['title' => 'Tema', 'text' => 'Elige tu tema preferido'], ]); // Get all settings with translated labels for a locale $settings = Settings::allWithTranslations('ca');
Events
A SettingUpdated event is dispatched after every set() and forget() call:
use Metalinked\LaravelSettingsKit\Events\SettingUpdated; Event::listen(SettingUpdated::class, function (SettingUpdated $event) { // $event->key, $event->value (null on forget/reset), $event->userId Log::info("Setting '{$event->key}' changed", ['value' => $event->value, 'user' => $event->userId]); });
Blade Directive
<title>@setting('site_name')</title> <body class="theme-@setting('theme', auth()->id())">
Artisan Commands
# List all settings php artisan settings:list php artisan settings:list --category=system php artisan settings:list --type=boolean # Export to JSON php artisan settings:export php artisan settings:export --file=settings.json --category=system # Import from JSON php artisan settings:import settings.json php artisan settings:import settings.json --force # overwrite existing php artisan settings:import settings.json --dry-run # preview only # Clear the settings cache (works with any cache driver) php artisan settings:clear-cache
REST API
Enable the API in your .env:
SETTINGS_KIT_API_ENABLED=true SETTINGS_KIT_API_AUTH=token SETTINGS_KIT_API_TOKEN=your-secure-token
For local development, you can bypass authentication entirely:
SETTINGS_KIT_API_ENABLED=true SETTINGS_KIT_API_DISABLE_AUTH_DEV=true
Endpoints
GET /api/settings-kit List all settings
GET /api/settings-kit/categories List categories
POST /api/settings-kit/preferences Create a preference
GET /api/settings-kit/global/{key} Get a global setting value
POST /api/settings-kit/global/{key} Update a global setting value
PUT /api/settings-kit/global/{key} Update a global setting value
DELETE /api/settings-kit/global/{key} Permanently delete a preference
GET /api/settings-kit/user?user_id=123 All customisable settings for a user (resolved values)
POST /api/settings-kit/user/batch Update multiple user settings in one request
DELETE /api/settings-kit/user?user_id=123 Reset all overrides for a user to global defaults
GET /api/settings-kit/user/{key}?user_id=123 Get a user setting
POST /api/settings-kit/user/{key}?user_id=123 Update a user setting
PUT /api/settings-kit/user/{key}?user_id=123 Update a user setting
DELETE /api/settings-kit/user/{key}?user_id=123 Reset a user setting to the global default
Query parameters: locale (for translated labels), role, category, user_id.
Authentication
| Mode | Config | Use case |
|---|---|---|
| Static token | SETTINGS_KIT_API_AUTH=token, SETTINGS_KIT_API_TOKEN=your-token |
Server-to-server integrations (full access) |
| Laravel Sanctum | SETTINGS_KIT_API_AUTH=sanctum |
User-facing SPAs |
| Laravel Passport | SETTINGS_KIT_API_AUTH=passport |
OAuth applications |
| Dev bypass | SETTINGS_KIT_API_DISABLE_AUTH_DEV=true |
Local development only |
Note: Token mode grants full access to all users' settings. Use Sanctum or Passport for user-facing APIs where each user should only access their own settings.
Batch update example
POST /api/settings-kit/user/batch { "user_id": 123, "settings": { "theme": "dark", "language": "ca", "notifications": true } }
Response format
All responses include a success boolean:
{
"success": true,
"data": {
"theme": {
"value": "dark",
"is_overridden": true,
"type": "select",
"category": "appearance",
"options": ["light", "dark", "auto"],
"key": "theme"
}
},
"meta": { "count": 1, "user_id": 123 }
}
Error responses:
{ "success": false, "error": "Setting not found" }
Configuration
# Cache (works with any driver: file, database, redis, etc.) SETTINGS_KIT_CACHE_ENABLED=true SETTINGS_KIT_CACHE_TTL=3600 # Locale fallback when a translation is missing SETTINGS_KIT_FALLBACK_LOCALE=en # API SETTINGS_KIT_API_ENABLED=false SETTINGS_KIT_API_PREFIX=api/settings-kit SETTINGS_KIT_API_AUTH=token SETTINGS_KIT_API_TOKEN= SETTINGS_KIT_API_DISABLE_AUTH_DEV=false SETTINGS_KIT_API_AUTO_CREATE=false
Full config reference after publishing (config/settings-kit.php):
return [ 'cache' => [ 'enabled' => env('SETTINGS_KIT_CACHE_ENABLED', true), 'ttl' => env('SETTINGS_KIT_CACHE_TTL', 3600), 'prefix' => env('SETTINGS_KIT_CACHE_PREFIX', 'settings_kit'), ], 'tables' => [ // Warning: 'preferences' is a common table name. Rename if your app // already has a table with this name before running migrations. 'preferences' => 'preferences', 'preference_contents' => 'preference_contents', 'user_preferences' => 'user_preferences', ], 'fallback_locale' => env('SETTINGS_KIT_FALLBACK_LOCALE', 'en'), 'user_model' => 'App\Models\User', 'api' => [ 'enabled' => env('SETTINGS_KIT_API_ENABLED', false), 'prefix' => env('SETTINGS_KIT_API_PREFIX', 'api/settings-kit'), 'auth_mode' => env('SETTINGS_KIT_API_AUTH', 'token'), 'token' => env('SETTINGS_KIT_API_TOKEN'), 'disable_auth_in_development' => env('SETTINGS_KIT_API_DISABLE_AUTH_DEV', false), 'auto_create_missing_settings' => env('SETTINGS_KIT_API_AUTO_CREATE', false), ], ];
Facade Reference
| Method | Description |
|---|---|
get(key, userId?) |
Get a setting value |
getMultiple(keys[], userId?) |
Get multiple settings in one query |
remember(key, default, userId?) |
Get a value, auto-creating it with the default if missing |
set(key, value, userId?, autoCreate?) |
Set a value |
setMultiple(keyValues[], userId?) |
Set multiple values in one call |
setWithAutoCreate(key, value, userId?) |
Set a value, creating the preference if needed |
isEnabled(key, userId?) |
Boolean check |
forget(key, userId) |
Remove a user's override, reverting to the global default |
forgetAll(userId) |
Remove all overrides for a user, returns count |
delete(key) |
Permanently delete a preference and all its data |
count(category?, role?) |
Count preferences |
has(key) / exists(key) |
Check existence |
all(role?, userId?, category?) |
All settings as array |
allWithTranslations(locale?, role?, userId?, category?) |
All settings with translated labels |
allForUser(userId, locale?, category?) |
All customisable settings for a user, with is_overridden flag |
getUserOverrides(userId) |
Only the settings a user has explicitly overridden |
getByCategory(category, userId?, role?) |
Settings for a category |
getCategories() |
Available category names |
label(key, locale?) |
Translated label |
description(key, locale?) |
Translated description |
create(data[]) |
Create a preference |
createIfNotExists(key, data[]) |
Create only if absent |
createWithTranslations(key, data[], translations[]) |
Create with i18n content |
addTranslations(key, translations[]) |
Add or update translations |
clearAllCache() |
Clear all cached values (works with any driver) |
Testing
composer test
Contributing
Please see CONTRIBUTING.md for details.
License
MIT © Metalinked