I this post I am going to share one feature which most of the application have these days to change certain settings using UI, it can be implemented in many ways but one way I find doing is to store settings in the database and provide an auto-generated form to change the settings.
What are we building?
We will be building a setting management system which will be easily customizable and you can use it in any app you want to give the option to change the settings on the fly using a form UI.
We will create a config file where we can define all the options you want to give the user as settings. Then we will create a route which will show the defined option from the config file in a form, upon hitting save settings we will update it in the database.
Next, we will be adding a helper function setting($key, $default = null)
to access the stored settings.
Create Laravel App
Let’s start by creating a brand new application in laravel 5.5.
composer create-project --prefer-dist laravel/laravel db-settings
Once it’s installed we need auth scaffolded, run php artisan make:auth
to generate scaffolding, before migrating make sure you have configured database. add your credentials in .env file.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dbsetting
DB_USERNAME=root
DB_PASSWORD=secret
Now run the php artisan migrate
it will create the tables for migration and give you auth routes setup. Go ahead and register a new user and login.
Settings model and migration
To store settings in the database we need a table with 3 fields:
name: unique name string used as a key for settings
val: value of the setting, it will be a text column
type: date type will be used to cast the value to string
, integer
or boolean
etc.
Run php artisan make:model Setting -mc
to create the migration and controller for same. Edit the settings migration file in database/migrations and add above columns:
public function up()
{
Schema::create('settings', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('val');
$table->char('type', 20)->default('string');
$table->timestamps();
});
}
That’s it for migration, let’s move on to the settings config file, we will get back to Setting
model to implement all the functionality later.
Create setting_fields in Config
We have our laravel installation, let’s create a config file config/setting_fields.php
and add the following array.
return [
'app' => [
'title' => 'General',
'desc' => 'All the general settings for application.',
'icon' => 'glyphicon glyphicon-sunglasses',
'elements' => [
[
'type' => 'text', // input fields type
'data' => 'string', // data type, string, int, boolean
'name' => 'app_name', // unique name for field
'label' => 'App Name', // you know what label it is
'rules' => 'required|min:2|max:50', // validation rule of laravel
'class' => 'w-auto px-2', // any class for input
'value' => 'CoolApp' // default value if you want
]
]
],
'email' => [
'title' => 'Email',
'desc' => 'Email settings for app',
'icon' => 'glyphicon glyphicon-envelope',
'elements' => [
[
'type' => 'email',
...
],
[
...
],
[
...
]
]
],
]
If you see the above array we have defined our settings into sections, first top-level element in the array is app
, and under this, we have its meta information like title
and description
, the main part is the elements array, it defines all the input fields needed as form input elements.
Setting Model
This is the backbone of settings, we will add some methods on this model which will give a similar API as laravel config does, for example, you will be able to call Setting::set('key', 'value')
to set a value in settings and Setting::get('key')
to get a setting value.
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* Add a settings value
*
* @param $key
* @param $val
* @param string $type
* @return bool
*/
public static function add($key, $val, $type = 'string')
{
if ( self::has($key) ) {
return self::set($key, $val, $type);
}
return self::create(['name' => $key, 'val' => $val, 'type' => $type]) ? $val : false;
}
/**
* Get a settings value
*
* @param $key
* @param null $default
* @return bool|int|mixed
*/
public static function get($key, $default = null)
{
if ( self::has($key) ) {
$setting = self::getAllSettings()->where('name', $key)->first();
return self::castValue($setting->val, $setting->type);
}
return self::getDefaultValue($key, $default);
}
/**
* Set a value for setting
*
* @param $key
* @param $val
* @param string $type
* @return bool
*/
public static function set($key, $val, $type = 'string')
{
if ( $setting = self::getAllSettings()->where('name', $key)->first() ) {
return $setting->update([
'name' => $key,
'val' => $val,
'type' => $type]) ? $val : false;
}
return self::add($key, $val, $type);
}
/**
* Remove a setting
*
* @param $key
* @return bool
*/
public static function remove($key)
{
if( self::has($key) ) {
return self::whereName($key)->delete();
}
return false;
}
/**
* Check if setting exists
*
* @param $key
* @return bool
*/
public static function has($key)
{
return (boolean) self::getAllSettings()->whereStrict('name', $key)->count();
}
/**
* Get the validation rules for setting fields
*
* @return array
*/
public static function getValidationRules()
{
return self::getDefinedSettingFields()->pluck('rules', 'name')
->reject(function ($val) {
return is_null($val);
})->toArray();
}
/**
* Get the data type of a setting
*
* @param $field
* @return mixed
*/
public static function getDataType($field)
{
$type = self::getDefinedSettingFields()
->pluck('data', 'name')
->get($field);
return is_null($type) ? 'string' : $type;
}
/**
* Get default value for a setting
*
* @param $field
* @return mixed
*/
public static function getDefaultValueForField($field)
{
return self::getDefinedSettingFields()
->pluck('value', 'name')
->get($field);
}
/**
* Get default value from config if no value passed
*
* @param $key
* @param $default
* @return mixed
*/
private static function getDefaultValue($key, $default)
{
return is_null($default) ? self::getDefaultValueForField($key) : $default;
}
/**
* Get all the settings fields from config
*
* @return Collection
*/
private static function getDefinedSettingFields()
{
return collect(config('setting_fields'))->pluck('elements')->flatten(1);
}
/**
* caste value into respective type
*
* @param $val
* @param $castTo
* @return bool|int
*/
private static function castValue($val, $castTo)
{
switch ($castTo) {
case 'int':
case 'integer':
return intval($val);
break;
case 'bool':
case 'boolean':
return boolval($val);
break;
default:
return $val;
}
}
/**
* Get all the settings
*
* @return mixed
*/
public static function getAllSettings()
{
return self::all();
}
}
That’s lots of code, everything is self-explanatory, you can see I am getting all the settings stored in the database, and from getDefinedSettingFields()
method accessing setting_fields
config as a collection object, Next am plucking default value
validation rules
, casting type
, values for form input field.
Settings Route
We can now move on to setting the route for our settings page, let’s add it routes/web.php
.
Route::get('/home', 'HomeController@index')->name('home');
Route::group(['middleware' => 'auth'], function () {
Route::get('/settings', 'SettingController@index')->name('settings');
Route::post('/settings', 'SettingController@store')->name('settings.store');
});
Settings Controller
The Controller will have two methods, index and store. run php artisan make:controller SettingController
to create it, now open and add this.
public function index()
{
return view('setting.index');
}
public function store(Request $request)
{
$rules = Setting::getValidationRules();
$data = $this->validate($request, $rules);
$validSettings = array_keys($rules);
foreach ($data as $key => $val) {
if (in_array($key, $validSettings)) {
Setting::add($key, $val, Setting::getDataType($key));
}
}
return redirect()->back()->with('status', 'Settings has been saved.');
}
Index method is pretty simple, it just returns a view, store method handles actual database persistence logic, It gets the validation rules from config by Setting::getValidationRules()
, then it just loops over the request data and adds it in setting if a setting is defined in config file.
Our Setting::add($key)
method first checks if setting with the name already exists, if yes it simply updates it otherwise it creates a new setting with given key.
Settings View
Now we can focus on rendering all the fields defined in config/setting_fields.php
I will use a bootstrap panel for each section, and inside this panels body we will loop over all the fields from elements
array.
Create a new view resources/views/setting/index.blade.php
and add following markup:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
<form method="post" action="{{ route('settings.store') }}" class="form-horizontal" role="form">
{!! csrf_field() !!}
@if(count(config('setting_fields', [])) )
@foreach(config('setting_fields') as $section => $fields)
<div class="panel panel-info">
<div class="panel-heading">
<i class="{{ array_get($fields, 'icon', 'glyphicon glyphicon-flash') }}"></i>
{{ $fields['title'] }}
</div>
<div class="panel-body">
<p class="text-muted">{{ $fields['desc'] }}</p>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-7 col-md-offset-2">
@foreach($fields['elements'] as $field)
@includeIf('setting.fields.' . $field['type'] )
@endforeach
</div>
</div>
</div>
</div>
<!-- end panel for {{ $fields['title'] }} -->
@endforeach
@endif
<div class="row m-b-md">
<div class="col-md-12">
<button class="btn-primary btn">
Save Settings
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
Everything is basic HTML in a loop, but notice this part:
@foreach($fields['elements'] as $field)
@includeIf('setting.fields.' . $field['type'] )
@endforeach
I have extracted all the fields type in there own partials, it keeps your views clean and maintainable, we could have used if and else handle this rendering but keeping it in separate view partials make it a lot easier to organize. And you can add as many types as you want, you just need to define an element in config with ‘type’ => ‘datepicker’ or anything and create a partial in resources/views/setting/fields/datepicker.blade.php
to handle all the rendering in this file.
Now lets see how a fileds type view partials looks like:
Input type text view
<div class="form-group {{ $errors->has($field['name']) ? ' has-error' : '' }}">
<label for="{{ $field['name'] }}">{{ $field['label'] }}</label>
<input type="{{ $field['type'] }}"
name="{{ $field['name'] }}"
value="{{ old($field['name'], \setting($field['name'])) }}"
class="form-control {{ array_get( $field, 'class') }}"
id="{{ $field['name'] }}"
placeholder="{{ $field['label'] }}">
@if ($errors->has($field['name'])) <small class="help-block">{{ $errors->first($field['name']) }}</small> @endif
</div>
And as you know input type email
, number
, date
etc are very similar, just change the type property on the element will give us the input type, for example, to allow input type email we just need to create a partial call resources/views/setting/fields/email.blade.php
and inside it just add following:
Input Email view
@include('setting.fields._text')
And the same thing is for number
, date
etc.
Input Select view
Create another partial inside fields/select.blade.php
and add following:
<div class="form-group {{ $errors->has($field['name']) ? ' has-error' : '' }}">
<label for="{{ $field['name'] }}">{{ $field['label'] }}</label>
<select name="{{ $field['name'] }}" class="form-control {{ array_get( $field, 'class') }}" id="{{ $field['name'] }}">
@foreach(array_get($field, 'options', []) as $val => $label)
<option @if( old($field['name'], \setting($field['name'])) == $val ) selected @endif value="{{ $val }}">{{ $label }}</option>
@endforeach
</select>
@if ($errors->has($field['name'])) <small class="help-block">{{ $errors->first($field['name']) }}</small> @endif
</div>
As you can see its pretty easy to customize it, for example, if you want to change it to work with another frontend framework like Bulma
, Foundation
or Tailwind CSS
you just need to change the markup and classes in fields partials.
Setting helper function
You might have noticed I have used setting($key)
helper function to get the stored value for that key in the database. Let’s add this helper function in our composer autoload.
Open the composer.json and in autoload object add files array you want to autoload.
...
"psr-4": {
"App\\": "app/"
},
"files": [
"app/Utils/helpers.php"
]
Next, create our helpers file in app/Utils/helpers.php
and add this function:
if (! function_exists('setting')) {
function setting($key, $default = null)
{
if (is_null($key)) {
return new \App\Setting\Setting();
}
if (is_array($key)) {
return \App\Setting\Setting::set($key[0], $key[1]);
}
$value = \App\Setting\Setting::get($key);
return is_null($value) ? value($default) : $value;
}
}
With that we have completed our settings management system, let’s serve the app and see, you should see the following screen with all the settings you defined, before hitting Save Settings you must migrate the database to create settings table.
Make some changes and hit Save Settings, check the database your settings will be saved, now you can access them anywhere in your application by calling Setting::get('setting_name')
or our helper function setting('setting_name')
.
But there is a problem, We are listing all settings and calling setting('setting_name')
multiple times which is making one query to the database for each call 🙁 that’s a lot of queries to get the settings.
Let’s add caching in the Setting model to avoid multiple queries to the database. Modify the getAllSettings()
method and add some more to handle the cache flushing etc.
/**
* Get all the settings
*
* @return mixed
*/
public static function getAllSettings()
{
return Cache::rememberForever('settings.all', function() {
return self::all();
});
}
/**
* Flush the cache
*/
public static function flushCache()
{
Cache::forget('settings.all');
}
/**
* The "booting" method of the model.
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::updated(function () {
self::flushCache();
});
static::created(function() {
self::flushCache();
});
}
We are caching all settings from the database and returning it, then we hooked into model events, created
, deleted
and updated
to flush the cache on any change so our settings will have updated value. It has solved multiple query issue.
I have used key ‘settings.all’, you should pic a unique key prefixed with some model ID for your app if your app offers settings based on user, team etc.
As always I have posted the complete source code for you on GitHub, have fun, implementing settings will be now a piece of cake, just change the definition in the config/setting_fields.php file and your settings page will reflect new fields 😎
Nice post! I really liked the end with the cache tricks with model events. I’ll surely implement this with my current projects.
There are many packages that ease creating options in Laravel by the way.
That’s the beauty of Laravel community, there are plenty of packages for every feature.
@saqueib:disqus can you please tell me how you would go about sending the data to the model’s events to you could delete the cache for that particular model? Thanks!
All the event receives current model as parameter
static::created(function($model) {
$model
});
I hope this helps
yes it helps. Thanks a lot! 😀
It s a great post . thanks alot. one question tho. how can i override the actual values in app.php like timezone, locale. how can i use setting(‘myvalue’) in app.php or any other files like services.php under config folder?
thanks
For this you will need to create a service provider or you can use AppServiceProvider and override any laravel config like this:
public function boot()
{
config(['app.timezone' => setting('timezone')]);
config(['app.locale' => setting('locale')]);
}
Give it a try
i will give it a try. thanks alot. Do you think that would be a good idea when you save the settings, first delete old settings and save the new ones? when you dont need certain conf fields any longer you delete it from the setting_fields file, but they stay in the database. just a thought. what do you think?
Yes you can add one step to clean up before creating settings, something like Setting::syncFields() method which can get all the fields from setting_fields and remove anything which is not found.
thanks alot!