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

niels / CodeSoftware / #apple,#developer

New team doesn’t show up in Apple Developer

So you invited someone to be a developer on your team, they accepted the invitation, they can use AppStore Connect, but not Apple Developer. What gives?

Organization

You must be an organization for your team members to get access to Apple Developer. If you are an individual developer, your team members get access to AppStore Connect, but not Apple Developer.

Certificates, Identifiers & Profiles

Still not working? Make sure you give your members the Access to Certificates, Identifiers & Profiles permission. Why? I guess because it’s the only reason someone, who is not the account holder, would need access to the Developer account.

Change your Currency

Still not working? Well, this fixed it for me:

  • Have your team member open AppStore Connect.
  • Click the drop-down in the top-right and choose Edit Profile.
  • Change the currency setting to something else.
  • Click Save.
  • Go back to developer.apple.com/account, reload the page and use the drop-down to switch to the new team!

No, I cannot explain this last one, but it worked for many!

0 comments

niels / Software / #laravel

Embedding Laravel Passport keys in .env

Heads up! This post is more than a year old.

When deploying a Laravel project to a server, in an ideal scenario, there are two primary steps you should follow:

  • Clone your git repository
  • Configure the .env file

However, the introduction of Laravel Passport seemed to disrupt this streamlined process. Every time I deployed to a new server, I found myself needing to manually copy the oauth-private.key and oauth-public.key files.

In search of a solution to embed these keys within the .env file, I first stumbled upon this method on Laracasts. Although functional, it appeared somewhat untidy due to the double-escaped newline characters.

My pursuit for a cleaner solution led me to another approach on StackOverflow.


To avoid the hassle of manually reformatting these keys, I’ve crafted a convenient convert.php script for the task:

<?php

$privateKeyContent = str_replace(["-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----", "\r", "\n"], ["", "", "", ""], trim(file_get_contents('storage/oauth-private.key')));
$publicKeyContent = str_replace(["-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----", "\r", "\n"], ["", "", "", ""], trim(file_get_contents('storage/oauth-public.key')));

echo "PASSPORT_PRIVATE_KEY=\"-----BEGIN PRIVATE KEY-----\n{$privateKeyContent}\n-----END PRIVATE KEY-----\"\n";
echo "PASSPORT_PUBLIC_KEY=\"-----BEGIN PUBLIC KEY-----\n{$publicKeyContent}\n-----END PUBLIC KEY-----\"";
0 comments

niels / Software / #laravel

Redis Sentinel with Laravel

Heads up! This post is more than a year old.

As the use of Redis Sentinel with Laravel does not appear to be documented at the time of writing, I’m sharing the configuration I’m using.

This is the redis section of my config/database.php:

    'redis' => [

        'client' => env('REDIS_CLIENT', 'predis'),

        'options' => [

            // Don't set 'cluster' option. The only valid values are 'predis' and 'redis',
            // both conflict with sentinel replication.
            // 'cluster' => env('REDIS_CLUSTER', 'predis'),

            'prefix' => Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_',
        ],

        'default' => env('REDIS_SENTINELS', false) ? array_merge(
            array_map(function ($host) {
                return "tcp://{$host}:26379?timeout=0.1";
            }, explode(',', env('REDIS_SENTINELS', ''))),
            [
                'options' => [
                    'replication' => 'sentinel',
                    'service' => 'mymaster',
                    'parameters' => [
                        'database' => 0,
                        'password' => env('REDIS_PASSWORD', null),
                    ],
                ],
            ]
        ) : [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => 0,
        ],

        'cache' => env('REDIS_SENTINELS', false) ? array_merge(
            array_map(function ($host) {
                return "tcp://{$host}:26379?timeout=0.1";
            }, explode(',', env('REDIS_SENTINELS', ''))),
            [
                'options' => [
                    'replication' => 'sentinel',
                    'service' => 'mymaster',
                    'parameters' => [
                        'database' => 1,
                        'password' => env('REDIS_PASSWORD', null),
                    ],
                ],
            ]
        ) : [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => 1,
        ],

    ],

And this is what I have in my .env:

REDIS_SENTINELS=10.62.0.5,10.62.0.9,10.62.0.11,10.62.0.12
REDIS_HOST=10.62.0.5
REDIS_PASSWORD=null
REDIS_PORT=6379

Summary of what it does:

  • Define REDIS_SENTINELS and it will use the Sentinel configuration.
  • Omit or set to false REDIS_SENTINELS and it will use the regular stand-alone Redis.
  • Both Sentinel and non-Sentinel Laravel instances can operate in a mixed environment due to sharing the same prefixes.
0 comments