rahimi-ali/settings

Typed, validated, persistent application settings for Laravel.

Maintainers

Package info

github.com/rahimi-ali/settings

pkg:composer/rahimi-ali/settings

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-13 08:28 UTC

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

  • StringSettingType
  • IntegerSettingType
  • FloatSettingType
  • BooleanSettingType
  • ArraySettingType, constructed with the type of each element
  • OptionsSettingType, 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.