schoolpalm / app-settings
A lightweight settings abstraction package providing a consistent API for reading and writing application settings without exposing how or where those settings are stored.
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/support: ^12.0
- schoolpalm/cache-store: *
Requires (Dev)
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
- phpunit/phpunit: ^11.0
Suggests
- schoolpalm/cache-store: Recommended for high-performance cached settings with multi-tenant and multi-school support.
This package is auto-updated.
Last update: 2026-07-27 21:56:17 UTC
README
A lightweight, storage-agnostic settings abstraction package for Laravel that provides a consistent, expressive API for reading and writing application settings — without exposing how or where those settings are stored.
The package does not know anything about:
- Database engines
- Eloquent Models
- Tenants or Schools
- Caching mechanisms (Redis, Laravel Cache, etc.)
- Any specific storage engine
Instead, the package delegates all persistence responsibilities to a layered architecture: Facade → Manager → Builder → Resolver → Repository.
Table of Contents
- Installation
- Configuration
- Architecture
- Quick Start
- Basic CRUD Operations
- Fluent Builder API
- Settings Scope
- Context Isolation
- Settings Groups
- Database Connections
- Cache Integration
- Bulk Operations
- Setting Value Types
- Exception Handling
- Migrations
- Testing
- Complete API Reference
- Design Philosophy
Installation
Install via Composer
composer require schoolpalm/app-settings
Laravel Auto-Discovery
The package's service provider and facade are automatically discovered by Laravel. No manual registration is required.
- Service Provider:
SchoolPalm\AppSettings\Providers\AppSettingsServiceProvider - Facade:
SchoolPalm\AppSettings\Facades\AppSettings
The AppSettings facade is available globally after installation.
Publishing Configuration
php artisan vendor:publish --tag=app-settings-config
This publishes the configuration file to config/app-settings.php.
Publishing Migrations
# Publish main migration (standard applications) php artisan vendor:publish --tag=app-settings-migrations # Publish tenant migration (multi-tenant applications) php artisan vendor:publish --tag=app-settings-tenant-migrations
Run migrations after publishing:
php artisan migrate
Suggested Package
For high-performance cached settings with multi-tenant and multi-school support:
composer require schoolpalm/cache-store
This enables cache-first resolution while keeping the app-settings package completely decoupled from caching logic.
Configuration
Publish the configuration file and customize it:
<?php return [ /* |-------------------------------------------------------------------------- | Default Database Connection |-------------------------------------------------------------------------- | | The database connection used when no connection is explicitly provided. | This allows the package to work in normal Laravel applications without | requiring multi-tenancy. | | Examples: 'mysql', 'sqlite', 'tenant' | | In SchoolPalm this can normally remain null because the tenant | connection is provided dynamically. */ 'connection' => env('APP_SETTINGS_CONNECTION', config('database.default')), /* |-------------------------------------------------------------------------- | Default Context |-------------------------------------------------------------------------- | | Optional default context applied when no context is explicitly provided. | | Example: ['type' => 'school', 'id' => 1] */ 'context' => null, /* |-------------------------------------------------------------------------- | Default Group |-------------------------------------------------------------------------- | | Optional default settings group. */ 'group' => null, /* |-------------------------------------------------------------------------- | Settings Key Separator |-------------------------------------------------------------------------- | | Character used to separate key segments. */ 'key_separator' => env('APP_SETTINGS_KEY_SEPARATOR', '.'), /* |-------------------------------------------------------------------------- | Cache Configuration |-------------------------------------------------------------------------- | | Cache implementation is provided by cache-store. */ 'cache' => [ 'enabled' => env('APP_SETTINGS_CACHE', true), 'ttl' => env('APP_SETTINGS_CACHE_TTL', 3600), ], /* |-------------------------------------------------------------------------- | Default Values |-------------------------------------------------------------------------- | | Global default values for settings keys. */ 'defaults' => [], /* |-------------------------------------------------------------------------- | Migration Mode |-------------------------------------------------------------------------- | | Controls the migration target: | | - 'central': Standard Laravel application. | - 'tenant': Multi-tenant applications. */ 'migration' => [ 'mode' => env('APP_SETTINGS_MIGRATION_MODE', 'central'), ], ];
Architecture
High-Level Architecture
Application
│
▼
AppSettings Facade
│
▼
SettingsManager
│
▼
SettingsBuilder
│
▼
SettingsResolver
│
┌───────┴───────┐
▼ ▼
SettingsRepository CacheStore
│ │
▼ ▼
Database Redis / Cache
Layer Responsibilities
| Layer | Responsibility |
|---|---|
| Facade | Developer-friendly static API (AppSettings::get()) |
| SettingsManager | Entry point, delegates to Builder |
| SettingsBuilder | Fluent API, manages SettingsScope |
| SettingsResolver | Cache-first resolution strategy |
| SettingsRepository | Database persistence via Eloquent |
| SettingsScope | Value object for operation context |
Key Design Principle
The package only communicates with the database through SettingsRepository. It never directly knows about:
- Which database engine is used
- How caching is implemented
- What tenant/school resolution strategy the application uses
- How cache keys are generated
This keeps the package reusable, testable, and completely decoupled from application infrastructure.
Quick Start
use SchoolPalm\AppSettings\Facades\AppSettings; // Store a setting AppSettings::put('school.name', 'Emma High School'); // Retrieve a setting $name = AppSettings::get('school.name'); // Result: 'Emma High School' // Retrieve with a default value $motto = AppSettings::get('school.motto', 'Excellence'); // Result: 'Excellence' (since 'school.motto' doesn't exist) // Check if a setting exists if (AppSettings::has('school.name')) { // Setting exists } // Delete a setting AppSettings::forget('school.name'); // Delete all settings AppSettings::flush(); // Get all settings $allSettings = AppSettings::all();
Basic CRUD Operations
Store a Setting (put)
AppSettings::put('school.name', 'Emma High School'); AppSettings::put('school.enrollment', 1200); AppSettings::put('school.is_active', true); AppSettings::put('school.subjects', ['Math', 'Science', 'English']);
The put() method returns $this for chaining:
AppSettings::put('school.name', 'Emma High') ->put('school.email', 'info@emma.sc.ug') ->put('school.phone', '+256700000000');
Retrieve a Setting (get)
// Simple retrieval $name = AppSettings::get('school.name'); // With default value $motto = AppSettings::get('school.motto', 'Default Motto'); // Type-cast values are preserved $enrollment = AppSettings::get('school.enrollment'); // (int) 1200 $isActive = AppSettings::get('school.is_active'); // (bool) true
Check if Setting Exists (has)
if (AppSettings::has('school.name')) { // The setting exists in storage }
Note:
has()checks storage only. It does not consider default values.
Delete a Setting (forget)
AppSettings::forget('school.logo');
Returns $this for chaining:
AppSettings::forget('school.logo') ->forget('school.banner');
Retrieve All Settings (all)
$settings = AppSettings::all(); // Returns: ['school.name' => 'Emma High', 'school.enrollment' => 1200, ...]
Delete All Settings (flush)
AppSettings::flush(); // Removes ALL settings in the current scope
Fluent Builder API
The package provides a fluent builder for more complex operations. The builder methods can be chained to build up a SettingsScope, and then perform an operation.
Connection → Context → Group → Action
AppSettings::connection('tenant') ->context('school', 10) ->group('report_cards') ->put('show_photo', true); $value = AppSettings::connection('tenant') ->context('school', 10) ->group('report_cards') ->get('show_photo');
Using SettingsBuilder Directly via withScope
use SchoolPalm\AppSettings\Support\SettingsScope; $scope = new SettingsScope( connection: 'tenant', contextType: 'school', contextId: 10, group: 'report_cards' ); $builder = AppSettings::withScope($scope); $value = $builder->get('show_photo');
Method Chaining on Manager
The SettingsManager itself returns $this from mutating methods:
AppSettings::put('theme.color', 'blue') ->put('theme.font', 'Arial') ->put('theme.size', 14); AppSettings::forget('theme.color') ->forget('theme.font');
Settings Scope
SettingsScope is a value object that defines the isolation boundaries for a setting operation. It is immutable — each modifier method returns a new instance.
Scope Properties
| Property | Type | Description |
|---|---|---|
connection |
`string | null` |
contextType |
`string | null` |
contextId |
`string | int |
group |
`string | null` |
cacheContext |
`mixed | null` |
Creating a Scope
use SchoolPalm\AppSettings\Support\SettingsScope; // Global scope (no isolation) $global = new SettingsScope(); // School-specific scope $school = new SettingsScope( contextType: 'school', contextId: 15 ); // Fully scoped $scoped = new SettingsScope( connection: 'tenant', contextType: 'school', contextId: 15, group: 'report_cards' );
Immutable Scope Modification
Each modifier returns a new instance:
$scope = new SettingsScope(); $tenantScope = $scope->withConnection('tenant'); $schoolScope = $tenantScope->withContext('school', 10); $groupScope = $schoolScope->withGroup('report_cards'); $cachedScope = $groupScope->withCacheContext(['tenant_abc', 'school_123']); // Original $scope remains unchanged
Helper Methods
$scope->connection(); // Get connection name $scope->contextType(); // Get context type $scope->contextId(); // Get context ID $scope->group(); // Get group name $scope->hasContext(); // Whether scope has a context $scope->hasGroup(); // Whether scope belongs to a group $scope->toArray(); // Convert scope to array (useful for debugging)
Context Isolation
Contexts allow you to isolate settings for specific entities (schools, branches, users, departments, etc.).
Why Contexts?
Without context, all settings are global. Contexts enable multi-tenant or multi-entity settings where each entity has its own set of values.
Using Contexts
// Store settings for different schools AppSettings::context('school', 1)->put('name', 'School One'); AppSettings::context('school', 2)->put('name', 'School Two'); // Retrieve settings per school $school1Name = AppSettings::context('school', 1)->get('name'); // 'School One' $school2Name = AppSettings::context('school', 2)->get('name'); // 'School Two'
How It Works
When a context is applied, the package queries the database with:
WHERE context_type = 'school' AND context_id = 10
Without context:
WHERE context_type IS NULL AND context_id IS NULL
Database Structure
| context_type | context_id | group | key | value |
|---|---|---|---|---|
| NULL | NULL | NULL | app.name | "My App" |
| school | 1 | report_cards | show_photo | true |
| school | 2 | report_cards | show_photo | false |
Custom Context Types
You can use any context type that fits your application:
AppSettings::context('branch', 5)->put('currency', 'UGX'); AppSettings::context('user', 42)->put('theme', 'dark'); AppSettings::context('department', 'engineering')->put('lead', 'John');
Settings Groups
Groups provide a way to organize related settings together.
Using Groups
// Store grouped settings AppSettings::group('report_cards') ->put('show_photo', true); AppSettings::group('report_cards') ->put('show_signature', false); // Retrieve grouped settings $showPhoto = AppSettings::group('report_cards') ->get('show_photo'); // true // Get all settings within a group $reportCardSettings = AppSettings::group('report_cards')->all(); // Returns: ['show_photo' => true, 'show_signature' => false]
Groups with Contexts
Groups and contexts compose naturally:
// School-specific report card settings AppSettings::context('school', 10) ->group('report_cards') ->put('show_photo', true); // School-specific grading settings AppSettings::context('school', 10) ->group('grading') ->put('pass_mark', 50);
Common Group Use Cases
report_cards— Report card display settingsgrading— Grade calculation rulessms— SMS notification settingspayroll— Payroll configurationtheme— Application themingnotifications— Notification preferences
Database Connections
The package supports dynamic database connections, enabling tenant isolation at the database level.
Using Connections
// Store on the default connection AppSettings::put('app.name', 'My App'); // Store on a specific tenant connection AppSettings::connection('tenant_abc') ->put('school.name', 'Emma High'); // Retrieve from a specific connection $name = AppSettings::connection('tenant_abc') ->get('school.name');
Connections with Contexts and Groups
AppSettings::connection('tenant_abc') ->context('school', 10) ->group('report_cards') ->put('show_photo', true);
How Connection Works
The connection is passed to the Eloquent model via setConnection():
$model = new Setting(); $model->setConnection('tenant_abc');
This allows each tenant to have its own database while using the same codebase.
Cache Integration
The package implements a cache-first resolution strategy through SettingsResolver, which integrates with schoolpalm/cache-store.
How Caching Works
- Read: Try cache first → if miss, read from DB → store in cache
- Write: Write to DB first → refresh cache
- Delete: Delete from DB → remove from cache
Cache Context
Cache context is independent from settings context. It controls how cache keys are isolated:
use SchoolPalm\AppSettings\Support\SettingsScope; // Explicit cache context for cache isolation $scope = new SettingsScope( contextType: 'school', contextId: 10, cacheContext: ['tenant_abc', 'school_123'] );
Configuring Cache
// config/app-settings.php 'cache' => [ 'enabled' => env('APP_SETTINGS_CACHE', true), 'ttl' => env('APP_SETTINGS_CACHE_TTL', 3600), // 1 hour ],
Cache Resolution Flow
get('school.name')
│
▼
CacheStore::remember('school.name', TTL, function() {
│
▼
SettingsRepository::get(scope, 'school.name')
│
▼
Database query
│
▼
Return value → stored in cache
});
Bulk Operations
Store Multiple Settings (putMany)
The SettingsBuilder provides putMany() for batch operations:
use SchoolPalm\AppSettings\Facades\AppSettings; AppSettings::context('school', 1) ->putMany([ 'name' => 'Emma High School', 'email' => 'info@emma.sc.ug', 'phone' => '+256700000000', 'is_active' => true, 'enrollment' => 1200, ]);
This is equivalent to calling put() for each key-value pair individually.
Setting Value Types
The package supports all common PHP types through JSON serialization:
| Type | Supported | Stored As |
|---|---|---|
string |
✅ | JSON string |
integer |
✅ | JSON number |
float |
✅ | JSON number |
boolean |
✅ | JSON boolean |
array |
✅ | JSON array |
object |
✅ | JSON object |
null |
✅ | JSON null |
Examples
AppSettings::put('name', 'Emma High'); // string AppSettings::put('count', 150); // integer AppSettings::put('score', 95.5); // float AppSettings::put('active', true); // boolean AppSettings::put('grades', ['A', 'B', 'C']); // array AppSettings::put('metadata', ['year' => 2024]); // associative array AppSettings::put('theme', null); // null
Values are cast to JSON when stored and decoded when retrieved, preserving type information.
Exception Handling
The package throws SchoolPalm\AppSettings\Exceptions\SettingsException for various error conditions.
Exception Types
invalidKey(string $key)
Thrown when an empty or invalid key is provided:
use SchoolPalm\AppSettings\Exceptions\SettingsException; try { AppSettings::put('', 'value'); } catch (SettingsException $e) { echo $e->getMessage(); // "Invalid settings key provided: "". Keys must be non-empty strings." }
unsupportedValue(mixed $value)
Thrown when an unsupported value type is stored:
try { AppSettings::put('key', fopen('file.txt', 'r')); // resource } catch (SettingsException $e) { echo $e->getMessage(); // "Unsupported settings value type: "resource". Only string, int, float, bool, array, null, and serializable objects are supported." }
missingService()
Thrown when no SettingsService implementation is registered (legacy contract):
try { // Attempt to use AppSettings without a registered service } catch (SettingsException $e) { echo $e->getMessage(); // "A SettingsService implementation must be registered before using AppSettings." }
Best Practices
use SchoolPalm\AppSettings\Facades\AppSettings; use SchoolPalm\AppSettings\Exceptions\SettingsException; try { $value = AppSettings::get('some.key', 'default'); AppSettings::put('another.key', $value); } catch (SettingsException $e) { report($e); // Handle gracefully }
Migrations
Database Table Structure
The settings table schema:
| Column | Type | Description |
|---|---|---|
id |
bigInteger (PK) | Primary key |
context_type |
string (nullable) | Context namespace (school, branch, etc.) |
context_id |
unsignedBigInteger (nullable) | Context identifier |
group |
string (nullable) | Settings group name |
key |
string | Setting key |
value |
json (nullable) | Setting value (supports all types) |
created_at |
timestamp | Creation timestamp |
updated_at |
timestamp | Last update timestamp |
Indexes
- Unique:
(context_type, context_id, group, key)— Prevents duplicate settings in the same scope - Index:
(context_type, context_id)— Optimizes context-scoped queries - Index:
(group)— Optimizes group-scoped queries
Publishing Migrations
# Standard application php artisan vendor:publish --tag=app-settings-migrations # Multi-tenant application php artisan vendor:publish --tag=app-settings-tenant-migrations # Run migrations php artisan migrate
Testing
Test Setup
The package uses Pest PHP for testing with Orchestra Testbench.
// Basic CRUD test it('stores and retrieves a setting', function () { AppSettings::put('school.name', 'Emma High School'); expect(AppSettings::get('school.name')) ->toBe('Emma High School'); }); // Default value test it('returns default value when setting does not exist', function () { expect(AppSettings::get('school.motto', 'Excellence')) ->toBe('Excellence'); }); // Existence check test it('checks if a setting exists', function () { AppSettings::put('timezone', 'Africa/Kampala'); expect(AppSettings::has('timezone'))->toBeTrue(); });
Context Isolation Tests
it('isolates settings by context', function () { AppSettings::context('school', 1)->put('name', 'School One'); AppSettings::context('school', 2)->put('name', 'School Two'); expect(AppSettings::context('school', 1)->get('name')) ->toBe('School One'); });
Group Tests
it('stores settings inside a group', function () { AppSettings::group('report_cards')->put('show_photo', true); expect(AppSettings::group('report_cards')->get('show_photo')) ->toBeTrue(); });
Running Tests
composer test
Complete API Reference
AppSettings Facade
The facade proxies all calls to SettingsManager.
| Method | Description | Returns |
|---|---|---|
get(string $key, mixed $default = null) |
Retrieve a setting value | mixed |
put(string $key, mixed $value) |
Store a setting value | SettingsManager (fluent) |
has(string $key) |
Check if a setting exists | bool |
forget(string $key) |
Delete a setting | SettingsManager (fluent) |
all() |
Retrieve all settings in current scope | array |
flush() |
Delete all settings in current scope | SettingsManager (fluent) |
connection(?string $connection) |
Set database connection | SettingsBuilder |
context(string $type, string|int|null $identifier = null) |
Set settings context | SettingsBuilder |
group(string $group) |
Set settings group | SettingsBuilder |
withScope(SettingsScope $scope) |
Create builder from existing scope | SettingsBuilder |
SettingsManager Methods
| Method | Description | Returns |
|---|---|---|
put(string $key, mixed $value) |
Store setting | static |
get(string $key, mixed $default = null) |
Retrieve setting | mixed |
has(string $key) |
Check existence | bool |
forget(string $key) |
Delete setting | static |
all() |
Retrieve all settings | array |
flush() |
Delete all settings | static |
connection(?string $connection) |
Set connection | SettingsBuilder |
context(string $type, string|int|null $identifier) |
Set context | SettingsBuilder |
group(string $group) |
Set group | SettingsBuilder |
withScope(SettingsScope $scope) |
Create builder from scope | SettingsBuilder |
SettingsBuilder Methods
| Method | Description | Returns |
|---|---|---|
connection(?string $connection) |
Set database connection | static |
context(string $type, string|int|null $identifier) |
Set context | static |
group(?string $group) |
Set group | static |
get(string $key, mixed $default = null) |
Retrieve setting | mixed |
put(string $key, mixed $value) |
Store setting | static |
putMany(array $settings) |
Store multiple settings | static |
has(string $key) |
Check existence | bool |
forget(string $key) |
Delete setting | static |
all() |
Retrieve all settings | array |
flush() |
Delete all settings | static |
scope() |
Get current SettingsScope | SettingsScope |
SettingsScope Methods
| Method | Description | Returns |
|---|---|---|
__construct(...) |
Create scope with all properties | void |
connection() |
Get database connection | `string |
contextType() |
Get context type | `string |
contextId() |
Get context identifier | string|int|null |
group() |
Get settings group | `string |
cacheContext() |
Get cache context | mixed |
hasContext() |
Whether scope has context | bool |
hasGroup() |
Whether scope has group | bool |
withConnection(?string $connection) |
Create scope with new connection | SettingsScope |
withContext(string $type, string|int|null $id) |
Create scope with new context | SettingsScope |
withGroup(?string $group) |
Create scope with new group | SettingsScope |
withCacheContext(mixed $context) |
Create scope with cache context | SettingsScope |
toArray() |
Convert scope to array | array |
SettingsException Static Factory Methods
| Method | Description |
|---|---|
invalidKey(string $key) |
Exception for invalid keys |
unsupportedValue(mixed $value) |
Exception for unsupported types |
missingService() |
Exception for missing service |
Design Philosophy
app-settings is an abstraction layer, not a storage engine.
Its only responsibility is to provide a clean, expressive, and fluent API for working with application settings while delegating persistence to a repository layer.
Core Principles
- Simple Developer API — Minimal, intuitive, and expressive methods
- Database Independent — Works with any database engine
- Tenant Independent — No built-in tenant assumptions
- School Independent — Flexible context system for any entity
- Storage Independent — Plug in any storage backend
- Cache Independent — Cache is optional and swappable
- Easy to Test — Clear separation of concerns enables easy mocking
- Framework Agnostic — Works with any Laravel application
Separation of Responsibilities
| App Settings Package | Application |
|---|---|
| Developer API | Database connection resolution |
| Facade & Manager | Tenant/School context resolution |
| SettingsBuilder | Cache strategy |
| SettingsResolver | Serialization logic |
| SettingsRepository | Cache key generation |
| SettingsScope | Business logic |
| Exceptions | Validation rules |
Integration Pattern
AppSettings (package)
│
▼
SettingsManager (package)
│
▼
SettingsBuilder → SettingsScope (package)
│
▼
SettingsResolver (package)
│
├── SettingsRepository (package) → Database
└── CacheStore (optional package) → Cache
Why This Approach?
- Testability: Each layer can be tested in isolation
- Flexibility: Swap storage without changing application code
- Reusability: Use the same API across different projects
- Maintainability: Clear boundaries between concerns
- Future-Proof: Add new features without breaking existing code
License
This package is open-sourced software licensed under the MIT license.
Contributors
Support
- Email: support@schoolpalm.com
- Issues: GitHub Issues
- Source: GitHub