rahimi-ali / settings
Typed, validated, persistent application settings for Laravel.
Requires
- php: ^8.3
- laravel/framework: ^13.0
Requires (Dev)
- captainhook/captainhook: ^5.29
- friendsofphp/php-cs-fixer: ^3.80
- larastan/larastan: ^3.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^11.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
This package is auto-updated.
Last update: 2026-08-13 08:34:53 UTC
README
Typed, validated, persistent application settings for Laravel 13.
Settings are declared in code, grouped into namespaces, and stored as values in the database. That keeps the available settings, types, defaults, validation, labels, and authorization rules reviewable while still allowing values to change at runtime.
Requirements
- PHP 8.3 or newer
- Laravel 13
Installation
Install the package through Composer:
composer require rahimi-ali/settings php artisan migrate
Laravel discovers the service provider and Settings facade automatically.
To customize storage, caching, or the optional HTTP API, publish the config:
php artisan vendor:publish --tag=settings-config
The package loads its migration automatically. If your application keeps vendor migrations under source control, publish it before migrating:
php artisan vendor:publish --tag=settings-migrations php artisan migrate
Define settings
Register definitions during application boot, after the package service provider has been registered. A small application service provider is usually the clearest owner:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use RahimiAli\Settings\Definitions\SettingDefinition; use RahimiAli\Settings\Definitions\SettingNamespaceDefinition; use RahimiAli\Settings\Definitions\Types\BooleanSettingType; use RahimiAli\Settings\Definitions\Types\IntegerSettingType; use RahimiAli\Settings\Facades\Settings; class ApplicationSettingsServiceProvider extends ServiceProvider { public function boot(): void { Settings::registerNamespace( (new SettingNamespaceDefinition( key: 'checkout', translationKey: 'settings.checkout.label', )) ->add(new SettingDefinition( key: 'maximum_items', type: new IntegerSettingType(), translationKey: 'settings.checkout.maximum_items', defaultValue: 20, rules: ['required', 'integer', 'min:1'], authorizeReadValue: fn (SettingDefinition $_): bool => true, authorizeReadDefinition: fn (SettingDefinition $_): bool => true, authorizeUpdate: fn (SettingDefinition $_): bool => auth()->user()?->can('manage-settings') === true, )) ->add(new SettingDefinition( key: 'guest_checkout', type: new BooleanSettingType(), translationKey: 'settings.checkout.guest_checkout', defaultValue: true, rules: ['required', 'boolean'], authorizeReadValue: fn (SettingDefinition $_): bool => true, authorizeReadDefinition: fn (SettingDefinition $_): bool => true, authorizeUpdate: fn (SettingDefinition $_): bool => auth()->user()?->can('manage-settings') === true, )), ); } }
Full keys use the namespace:key form, such as
checkout:maximum_items. Namespace and definition keys must be non-empty,
cannot contain the reserved : separator, and their combined full key cannot
exceed 512 characters. Every namespace may be registered only once; duplicate
registration throws a LogicException, making ownership conflicts visible
during boot.
Translation keys use Laravel's normal translation system. The optional
commentTranslationKey on a definition supplies presentation metadata for an
admin UI or API client.
Available types
StringSettingTypeIntegerSettingTypeFloatSettingTypeBooleanSettingTypeArraySettingType, constructed with the type of each elementOptionsSettingType, which adds option metadata around another type
Types are responsible for conversion to and from the stored string value.
StringSettingType accepts only strings, and ArraySettingType represents a
JSON list rather than an associative map.
Validation remains explicit in each definition's Laravel validation rules.
For an array's item rules, prefix the rule with *.; for example:
new SettingDefinition( key: 'notification_channels', type: new ArraySettingType(new StringSettingType()), translationKey: 'settings.checkout.notification_channels', defaultValue: ['mail'], rules: ['array', '*.string'], );
OptionsSettingType exposes available choices but does not implicitly validate
membership. Add an in rule (or another appropriate Laravel rule) when a value
must be restricted to those choices.
Read and write values
Use the facade:
use RahimiAli\Settings\Facades\Settings; $limit = Settings::get('checkout:maximum_items'); $fallback = Settings::get('checkout:missing', 10); $limit = Settings::getOrFail('checkout:maximum_items'); Settings::set('checkout:maximum_items', 30);
get() returns its optional default when the key is not registered or has no
stored value. getOrFail() throws SettingNotSetException in those cases.
Neither method substitutes the definition's declared default automatically;
populate defaults explicitly during deployment.
set() validates the value and checks the definition's update authorization
closure. It throws RuntimeException when either check fails. For code that
needs validation errors without an exception, use tryToSet():
$result = Settings::tryToSet('checkout:maximum_items', 0); if ($result !== true) { // $result is an Illuminate MessageBag. }
Authorization is intentionally denied when its closure is omitted. The
ignoreAuthorization argument exists for trusted application and maintenance
code; do not pass untrusted input to it.
Definition access is the higher read privilege: when
authorizeReadDefinition allows access, the definition response includes the
current value and authorizeReadValue is implicitly allowed. Use
authorizeReadValue by itself when a caller may read a value without seeing
its definition metadata.
The concrete SettingsManager can also be injected instead of using the
facade:
use RahimiAli\Settings\Services\SettingsManager; final readonly class CheckoutPolicy { public function __construct(private SettingsManager $settings) { } }
Populate and maintain settings
The package provides four Artisan commands:
# Store defaults for definitions that do not have a value yet. php artisan settings:populate # Replace all registered values with their declared defaults. php artisan settings:populate --reset # Skip the production confirmation for a deliberate non-interactive reset. php artisan settings:populate --reset --force # Read one value, or print all registered settings. php artisan settings:get checkout:maximum_items php artisan settings:get # Store a literal string. This trusted command bypasses authorization. php artisan settings:set checkout:label "Express checkout" # Decode typed values from JSON before validation. php artisan settings:set checkout:maximum_items 30 --json php artisan settings:set checkout:guest_checkout true --json php artisan settings:set checkout:notification_channels '["mail","sms"]' --json # Delete stored values whose definitions are no longer registered. # This asks for confirmation in production; --force skips that prompt. php artisan settings:cleanup php artisan settings:cleanup --force
settings:populate --reset and settings:cleanup are destructive maintenance
commands and ask for confirmation in production. Cleanup is refused when no
settings are registered, even with --force, because an incomplete
application boot must not turn into a table-wide deletion.
Caching
Values from the built-in Eloquent repository are cached by default. Publish the config to select a Laravel cache store, TTL, and key prefix, or disable caching:
'cache' => [ 'enabled' => true, 'store' => null, 'ttl' => 300, 'prefix' => 'settings:', ],
A second, short-lived near cache can reduce reads in long-running workers:
'near' => [ 'enabled' => true, 'store' => 'octane', 'ttl' => 30, ],
Cache entries contain both existence and value, so stored null and missing
rows remain distinct without repeated database queries. Writes rotate the
generation of each changed setting after the repository transaction commits,
without invalidating unrelated settings. A concurrent read can finish under an
old generation, but its result is no longer reachable by later reads. Cache
keys use hashes of setting keys so they remain safe for stores with restrictive
key-length limits. Cache TTLs must be positive and finite.
Custom repositories are not automatically wrapped in this cache decorator; they own their caching behavior. If another process writes directly to the settings table, readers may observe the old value until the configured TTL.
Optional HTTP API
The HTTP API is disabled by default. Enable it only after configuring authentication and authorization middleware:
'http' => [ 'enabled' => true, 'prefix' => 'settings', 'middleware' => ['api', 'auth:sanctum'], ],
It registers these routes under the configured prefix:
| Method | Path | Purpose |
|---|---|---|
GET |
/settings |
Authorized definition tree |
GET |
/settings/values |
Authorized values keyed by full key |
GET |
/settings/{key}/value |
One authorized value |
PUT |
/settings |
Validate the complete batch and update it atomically |
An importable OpenAPI 3.1 document for these routes is available at
docs/openapi.yaml. It uses the default /settings
prefix; update its paths if the application configures a different prefix.
The update payload is:
{
"settings": [
{"key": "checkout:maximum_items", "value": 30},
{"key": "checkout:guest_checkout", "value": true}
]
}
The middleware protects the route group; each definition's authorization closures control whether its definition, value, or update is exposed. The package does not guess your application's users, guards, or permissions. Definition-read permission includes value-read permission, as described above.
Events
Every real value change dispatches SettingUpdated after commit. The event
contains the key, whether a previous row existed, and canonical typed previous
and current values. No event is emitted for a no-op write. The built-in
repository captures those values inside the same locked transaction as the
write; a custom repository must return equally authoritative change snapshots
from setValuesByKey().
The event is an authoritative description of the successful mutation, not a durable audit log. Applications that require historical records must persist the event in their own listener and include their own actor and request context.
Custom storage
The published config supports a custom Eloquent model and repository:
'model' => App\Models\Setting::class, 'repository' => App\Settings\CustomSettingRepository::class, 'table' => 'settings',
A custom model must extend Eloquent's Model and implement
RahimiAli\Settings\Contracts\SettingModel. This is an intentional nominal
opt-in marker; the application remains responsible for providing compatible
key and value columns.
The bundled migration uses the application's default database connection. A custom model on another connection requires an application-owned migration on that connection.
A custom repository must implement
RahimiAli\Settings\Contracts\SettingRepository. getByKey() returns one
complete lookup that distinguishes a missing row from a stored null.
setValuesByKey() must write the whole batch atomically and return one
authoritative previous/current storage snapshot for every supplied key. Custom
repositories own their transaction and cache behavior; the public settings API
remains unchanged.
Testing
For isolated tests, bind InMemorySettingRepository before resolving the
settings manager:
use RahimiAli\Settings\Contracts\SettingRepository; use RahimiAli\Settings\Repositories\InMemorySettingRepository; $this->app->singleton( SettingRepository::class, fn (): SettingRepository => new InMemorySettingRepository(), );
Run this package's suite with:
composer test
composer analyse
composer cs-check
License
Settings is open-source software licensed under the MIT License.