niels / Software / #laravel

Load Laravel configuration from the database

The .env and config files used by Laravel are a great way to separate configuration code from configuration values. However, managing multiple front-end servers requires duplicating your .env file and updating it every time you add or change a variable.

To solve this problem, I’ve created a simple solution that is fully transparent to Laravel and leverages Laravel’s config caching.

Database Table

First, create a simple database table to store your settings.

<?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', 4096)->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('settings');
    }
};

Model

To add settings to the database, I created a basic Setting model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Setting extends Model
{
    protected $fillable = ['key', 'value'];

    protected $casts = [
        'value' => 'encrypted',
    ];
}

The settings are encrypted, but there’s no performance penalty since Laravel caches the configuration.

Seed

You can easily seed or add settings to the database:

Setting::firstOrCreate( [
            'key'   => 'mail.mailers.outbound.transport',
            'value' => 'postmark',
] );

Setting::firstOrCreate( [
            'key'   => 'nova.license_key',
            'value' => '1234',
] );

Config File

Finally, we install a custom config file named zettings.php to ensure it’s the last config file loaded. This file loads all settings from the database, overriding any settings from other config files.

<?php
/*
 * zettings.php
 *
 * Loads and overrides settings from the database.
 *
 * Use php artisan config:cache to cache the settings as usual.
 *
 * Ideally this is the last file to be loaded in the config directory.
 *
 */
$database = config('database.default');
$config = config('database.connections.' . $database);

try {
    if($database == 'sqlite') {
        $dsn = "{$config['driver']}:{$config['database']}";
        $pdo = new PDO($dsn);
    } else {
        $dsn = "{$config['driver']}:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['charset']}";
        $pdo = new PDO($dsn, $config['username'], $config['password']);
    }

    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $statement = $pdo->query("SELECT `key`, `value` FROM settings");
    $settings = $statement->fetchAll(PDO::FETCH_KEY_PAIR);

    $pdo = null;

    $key = config('app.key');
    $cipher = config('app.cipher');

    if (\Illuminate\Support\Str::startsWith($key, 'base64:')) {
        $key = base64_decode(substr($key, 7));
    }

    $encryptor = new \Illuminate\Encryption\Encrypter($key, $cipher);

    foreach ($settings as $key => $value) {
        config([$key => $encryptor->decryptString($value)]);
    }

    return [
        'loaded' => true,
    ];
} catch (PDOException $e) {
    return [
        'loaded' => false,
    ];
}

Limitations

The zettings.php file requires access to the database and encryption key, so some settings, notably the app and database settings, still need to be provided through the .env file. Fortunately, these settings rarely change. For everything else, you can use the database settings.

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *