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
Commented 2 months ago
Very helpful!