Store Laravel Global Settings in the Database (with Caching)

Store Laravel Global Settings in the Database (with Caching)
Admin
Friday, May 12, 2023 6 mins to read
Share
Store Laravel Global Settings in the Database (with Caching)

Do you store Laravel global settings in the config files? Would you want them to be editable without touching the code? Let's build a Settings page where you can change things quickly.

Database Settings

Notice: In this simple tutorial we won't use any packages for this, but at the end, I will give a few additional links for extra resources using packages.


1. Setup new Laravel project

For this tutorial, we will start with a new Laravel project with Laravel Breeze as a starter kit.

laravel new demo-database-settings
cd demo-database-settings
composer require laravel/breeze
php artisan breeze:install blade
php artisan migrate

2. DB Structure: Settings Model with Migration and Seeder

php artisan make:model Setting -mcs

Let's add the key and value columns to our migration file. We ensure that the key is always unique and that value can have a null value.

database/migrations/2023_05_12_040715_create_settings_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
return new class extends Migration {
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('value')->nullable();
$table->timestamps();
});
}
};

In the model, allow key and value attributes to be mass assignable by adding the $fillable property.

app/Models/Setting.php

namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Setting extends Model
{
use HasFactory;
 
protected $fillable = ['key', 'value'];
}

Let's add some initial entries into the seeder:

database/seeders/SettingSeeder.php

namespace Database\Seeders;
 
use App\Models\Setting;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
 
class SettingSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$settings = [
['key' => 'site_name', 'value' => 'Article about database settings'],
['key' => 'description', 'value' => 'Learn how to manage app settings using database.'],
['key' => 'admin_email', 'value' => 'admin@admin.com'],
['key' => 'posts_per_page', 'value' => 10],
['key' => 'users_can_register', 'value' => true],
];
 
Setting::insert($settings);
}
}

Make sure you call SettingSeeder by adding it to DatabaseSeeder.

database/seeders/DatabaseSeeder.php

namespace Database\Seeders;
 
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call(SettingSeeder::class);
}
}

Finally, you can run migrations with seeders:

php artisan migrate --seed

3. Controller, Routes, and View

Our Controller will have edit() and update() methods to show form and update values:

app/Http/Controllers/SettingsController.php

namespace App\Http\Controllers;
 
use App\Models\Setting;
use Illuminate\Http\Request;
 
class SettingsController extends Controller
{
public function edit()
{
return view('settings', [
'settings' => Setting::get(['key', 'value'])
]);
}
 
public function update(Request $request)
{
$data = $request->except('_token');
 
foreach ($data as $key => $value) {
Setting::where('key', $key)->update(['value' => $value]);
}
 
return to_route('settings.edit')
->withStatus('Settings updated successfully.');
}
}

Create the settings blade view with the following contents:

resources/views/settings.blade.php

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Settings') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
@if(session('status'))
<div class="p-3 mb-3 rounded border border-green-200 text-green-700 bg-green-50">{{ session('status') }}</div>
@endif
 
<form method="post" action="{{ route('settings.update') }}">
@csrf
@foreach($settings as $setting)
<div class="grid grid-cols-3 items-center odd:bg-gray-50">
<label class="p-2" for="{{ $setting->key }}">{{ $setting->key }}</label>
<input class="col-span-2 m-2" id="{{ $setting->key }}" name="{{ $setting->key }}" type="text" value="{{ $setting->value }}">
</div>
@endforeach
 
<button class="px-4 py-2 mt-3 bg-indigo-700 rounded text-white font-semibold">
Update Settings
</button>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

And add settings entry to the top navigation bar, replicating Laravel Breeze Blade component x-nav-link:

resources/views/layouts/navigation.blade.php

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
 
<x-nav-link :href="route('settings.edit')" :active="request()->routeIs('settings.edit')">
{{ __('Settings') }}
</x-nav-link>
</div>

Bind routes to SettingsController methods:

routes/web.php

use App\Http\Controllers\SettingsController;
 
Route::middleware('auth')->group(function () {
Route::get('/settings', [SettingsController::class, 'edit'])->name('settings.edit');
Route::post('/settings', [SettingsController::class, 'update'])->name('settings.update');
});

Finally, we can (re-)compile our assets using the npm command so views will be displayed properly:

npm install
npm run build

4. Caching Settings

To retrieve the value of the setting typically you would call:

Setting::where('key', 'site_name')->first();

This results in a new DB query each time! Instead, let's fetch all the values once and store them in Cache.

To cache values, update the boot method of AppServiceProvider with the following content:

app/Providers/AppServiceProvider.php

use App\Models\Setting;
use Illuminate\Support\Facades\Cache;
 
// ...
 
public function boot(): void
{
$this->app->bind('settings', function () {
return Cache::rememberForever('settings', function () {
return Setting::pluck('value', 'key');
});
});
}

Now, values can be retrieved directly from the app container like this:

app('settings')['site_name']

For changes to take effect after we update values from the settings page, we need to clear the settings cache in the Controller, so settings will be re-cached.

To do that, add the Cache::forget('settings') line after updating values in the update method:

app/Http/Controllers/SettingsController.php

use Illuminate\Support\Facades\Cache;
 
public function update(Request $request)
{
$data = $request->except('_token');
 
foreach ($data as $key => $value) {
Setting::where('key', $key)->update(['value' => $value]);
}
 
Cache::forget('settings');
 
return to_route('settings.edit')
->withStatus('Settings updated successfully.');
}

5. Helper for Retrieving the Settings

Directly accessing settings from the container is also not very convenient:

app('settings')['site_name']

What if we could do something and have access to the cached values like the config() helper?

Let's create a new helpers file with the following content:

app/Support/helpers.php

if (! function_exists('settings')) {
function settings(?string $key = null, $default = null)
{
if (is_null($key)) {
return app('settings');
}
 
return app('settings')->get($key, $default);
}
}

And add a path to autoload helpers in the composer.json file:

composer.json

"autoload": {
"files": [
"app/Support/helpers.php"
],
 
// ...
}

Now you will be able to access your settings globally without any queries to the database:

PHP

settings('admin_email')

BLADE

{{ settings('site_name') }}

For example, if you want to show the site name in the navigation, do this.

resources/views/layouts/navigation.blade.php

<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
<div class="ml-2 font-semibold">{{ settings('site_name') }}</div>
</div>

6. Adding More Settings via Migration

The final section with a bit of warning: dealing with cache is tricky, you may forget to clear it after some changes.

So, if you later decide to add more settings to your application using migrations, do not forget to clear the cache by adding the Cache::forget('settings'); line to the end of your migration.

use App\Models\Setting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
 
return new class extends Migration {
public function up(): void
{
$data = [
['key' => 'start_of_week', 'value' => 1],
['key' => 'show_testimonials', 'value' => true],
['key' => 'allow_comments', 'value' => false],
];
 
Setting::insert($data);
 
Cache::forget('settings');
}
};

To rephrase it, Cache::forget('settings'); is your FORCE UPDATE settings. You can also run it manually whenever needed, via php artisan tinker.


As I mentioned, we didn't use any packages for this tutorial, but here are a few of my other videos, with alternative approaches: