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.
- 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
composer require schoolpalm/app-settingsThe 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.
php artisan vendor:publish --tag=app-settings-configThis publishes the configuration file to config/app-settings.php.
# 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-migrationsRun migrations after publishing:
php artisan migrateFor high-performance cached settings with multi-tenant and multi-school support:
composer require schoolpalm/cache-storeThis enables cache-first resolution while keeping the app-settings package completely decoupled from caching logic.
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'),
],
]; Application
│
▼
AppSettings Facade
│
▼
SettingsManager
│
▼
SettingsBuilder
│
▼
SettingsResolver
│
┌───────┴───────┐
▼ ▼
SettingsRepository CacheStore
│ │
▼ ▼
Database Redis / Cache
| 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 |
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.
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();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');// 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) trueif (AppSettings::has('school.name')) {
// The setting exists in storage
}Note:
has()checks storage only. It does not consider default values.
AppSettings::forget('school.logo');Returns $this for chaining:
AppSettings::forget('school.logo')
->forget('school.banner');$settings = AppSettings::all();
// Returns: ['school.name' => 'Emma High', 'school.enrollment' => 1200, ...]AppSettings::flush(); // Removes ALL settings in the current scopeThe 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.
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');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');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');SettingsScope is a value object that defines the isolation boundaries for a setting operation. It is immutable — each modifier method returns a new instance.
| Property | Type | Description |
|---|---|---|
connection |
`string | null` |
contextType |
`string | null` |
contextId |
`string | int |
group |
`string | null` |
cacheContext |
`mixed | null` |
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'
);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$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)Contexts allow you to isolate settings for specific entities (schools, branches, users, departments, etc.).
Without context, all settings are global. Contexts enable multi-tenant or multi-entity settings where each entity has its own set of values.
// 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'When a context is applied, the package queries the database with:
WHERE context_type = 'school'
AND context_id = 10Without context:
WHERE context_type IS NULL
AND context_id IS NULL| 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 |
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');Groups provide a way to organize related settings together.
// 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 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);report_cards— Report card display settingsgrading— Grade calculation rulessms— SMS notification settingspayroll— Payroll configurationtheme— Application themingnotifications— Notification preferences
The package supports dynamic database connections, enabling tenant isolation at the database level.
// 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');AppSettings::connection('tenant_abc')
->context('school', 10)
->group('report_cards')
->put('show_photo', true);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.
The package implements a cache-first resolution strategy through SettingsResolver, which integrates with schoolpalm/cache-store.
- 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 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']
);// config/app-settings.php
'cache' => [
'enabled' => env('APP_SETTINGS_CACHE', true),
'ttl' => env('APP_SETTINGS_CACHE_TTL', 3600), // 1 hour
],get('school.name')
│
▼
CacheStore::remember('school.name', TTL, function() {
│
▼
SettingsRepository::get(scope, 'school.name')
│
▼
Database query
│
▼
Return value → stored in cache
});
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.
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 |
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); // nullValues are cast to JSON when stored and decoded when retrieved, preserving type information.
The package throws SchoolPalm\AppSettings\Exceptions\SettingsException for various error conditions.
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."
}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."
}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."
}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
}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 |
- 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
# 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 migrateThe 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();
});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');
});it('stores settings inside a group', function () {
AppSettings::group('report_cards')->put('show_photo', true);
expect(AppSettings::group('report_cards')->get('show_photo'))
->toBeTrue();
});composer testThe 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 |
| 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 |
| 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 |
| 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 |
| Method | Description |
|---|---|
invalidKey(string $key) |
Exception for invalid keys |
unsupportedValue(mixed $value) |
Exception for unsupported types |
missingService() |
Exception for missing service |
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.
- 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
| 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 |
AppSettings (package)
│
▼
SettingsManager (package)
│
▼
SettingsBuilder → SettingsScope (package)
│
▼
SettingsResolver (package)
│
├── SettingsRepository (package) → Database
└── CacheStore (optional package) → Cache
- 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
This package is open-sourced software licensed under the MIT license.
- Email: support@schoolpalm.com
- Issues: GitHub Issues
- Source: GitHub