Save laravel app settings in database

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.

Save laravel app settings in database

Source Code

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 valuevalidation 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.

settings-page

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 😎

Source Code