imran/laravel-dynamic-config

Dynamic runtime configuration system for Laravel applications.

Maintainers

Package info

github.com/grim-reapper/laravel-dynamic-config

pkg:composer/imran/laravel-dynamic-config

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-07-19 11:39 UTC

This package is auto-updated.

Last update: 2026-07-19 11:40:03 UTC


README

Latest Version on Packagist Total Downloads License

Take control of your Laravel application configuration at runtime. Load, merge, and override configuration from multiple sources like Databases, YAML, JSON, and PHP files without redeploying your app.

🌟 Why Laravel Dynamic Config?

In modern web applications, configuration often needs to be more than just static .env or PHP files. Whether it's white-labeling settings, feature flags stored in a database, or environment-specific overrides in YAML/JSON, managing these can become a nightmare.

The Problem

  • Deployment Overhead: Changing a simple config value often requires a full CI/CD pipeline run.
  • Database Settings: Storing settings in the database but struggling to inject them seamlessly into Laravel's config() system.
  • Complexity: Merging multiple configuration sources manually is error-prone and hard to maintain.

The Solution

Laravel Dynamic Config provides a robust, priority-based configuration engine that hooks directly into Laravel's core. It allows you to define multiple "sources" of truth and merges them intelligently, giving you a unified config() API while keeping your application fast with built-in caching.

✨ Features

  • 🛠 Multiple Drivers: Out-of-the-box support for Database, YAML, JSON, PHP, and a remote API driver.
  • 🧬 Priority Merging: Define which source takes precedence. Higher priority sources override lower ones.
  • 📦 Namespace Support: Load configuration into specific keys (e.g., services.stripe) or the root config.
  • 🌎 Environment Scoping: Limit a source to specific environments (e.g. only load the API driver in production).
  • 🛡 Protected Keys: Block dynamic sources from ever overriding sensitive config paths (app.key, DB credentials, etc.).
  • 🧯 Fails Soft: A broken source (down DB, malformed file, network error) is logged and skipped instead of crashing the app - configurable per-project.
  • âš¡ High Performance: Custom caching engine (atomic writes) ensures your app remains blazing fast.
  • 💻 CLI Power: Artisan commands for debugging, caching, and clearing configuration.
  • 🧩 Extensible: Easily add your own custom drivers.

🚀 Installation

You can install the package via composer:

composer require imran/laravel-dynamic-config

Publish the configuration file:

php artisan vendor:publish --tag=dynamic-config-config

If you plan to use the database driver, publish and run the migration for its default app_configs table (key/value columns):

php artisan vendor:publish --tag=dynamic-config-migrations
php artisan migrate

Note: If you intend to use YAML configurations, ensure you have the symfony/yaml package installed: composer require symfony/yaml

Configuration

Define your sources in config/dynamic-config.php:

return [
    'merge_strategy' => 'deep', // deep, replace, append
    'cache_file' => base_path('bootstrap/cache/dynamic_config.php'),
    
    'sources' => [
        [
            'driver'   => 'json',
            'priority' => 5,
            'path'     => storage_path('configs/app.json'),
        ],
        [
            'driver'   => 'yaml',
            'priority' => 10,
            'path'     => storage_path('configs/features.yaml'),
        ],
        [
            'driver'   => 'database',
            'priority' => 20,
            'table'    => 'app_configs',
        ],
    ],
];

Artisan Commands

Caching Configurations

Because dynamic configurations bypass Laravel's native config:cache, you must use this package's cache commands for production performance.

php artisan dynamic-config:cache

To clear the generated cache:

php artisan dynamic-config:clear

Debugging Resolutions

Ever wonder where a config value came from? Use the debug command to see a resolution tree for a specific key:

php artisan dynamic-config:debug app.name

Output:

Inspecting key: app.name

├── json[1]: "My App"
├── yaml[1]: "Enterprise App"
└── final: "Enterprise App"

Configuration Options

Key Default Purpose
merge_strategy deep deep, replace (array_replace_recursive), or append (array_merge_recursive).

With deep (the default), list-style arrays (app.providers, cors.allowed_origins, a middleware group, etc.) are replaced wholesale by whichever source sets them last, not merged index-by-index - so overriding a 3-item list with a 1-item list actually results in a 1-item list, and overriding with [] actually clears it. Associative arrays (database.connections.mysql.*, app.*, ...) still merge key-by-key as expected.

| fail_silently | true | Log and skip a source that fails to load instead of crashing the request/command. Set false in local/CI to catch broken sources immediately. | | auto_refresh | false | Detect when a file-based source changed since the last dynamic-config:cache run and rebuild live instead of serving stale cache. Adds a few filemtime() checks per request - fine for local/staging, leave off in production. | | protected_keys | [] | Dot-notation paths (wildcards supported) that no dynamic source may ever set. See Security below. | | protect_sensitive_keys | true | Automatically block any path whose last segment looks like a secret, even if it's not in protected_keys. See Security below. | | sensitive_key_exceptions | [] | Exact dot paths allowed to bypass protect_sensitive_keys for a deliberate use case (e.g. rotating one specific secret via the database driver). | | sources[].environments | — | Restrict a source to specific environments, e.g. 'environments' => ['production']. |

Security: the database (and api) driver trust boundary

Any row in your app_configs table (or any value returned by an api source) can set any config path - the merge doesn't know or care where a key came from. That's fine as long as only trusted code/administrators can write to that table or endpoint. If it's ever reachable through a less-trusted path (a tenant self-service settings screen, an admin panel with a mass-assignment bug, a compromised upstream API), that becomes a path to full config takeover. Three layers guard against this:

1. app.key and app.cipher are always protected, unconditionally, with no config option and no exception list that can override it. There's no legitimate reason for a runtime source to change Laravel's encryption key, and doing so would silently break decryption of every cookie/session/Crypt::encrypt() value already out there.

2. protect_sensitive_keys (on by default) blocks any path whose last segment looks like a secret - password, secret, token, credential, api_key, secret_key, private_key, access_key, client_secret, auth_token - anywhere in the tree, without you having to list every one by hand. This applies to every driver, not just database/api - a services.stripe.secret key checked into your own json/yaml/php dynamic-config file is blocked by default too, since the merge has no reliable way to know a file source is "trusted" while a database source isn't. If you deliberately want to set a secret through a dynamic source (file or DB), allow just that path:

'protect_sensitive_keys' => true,
'sensitive_key_exceptions' => [
    'services.stripe.secret',
],

3. protected_keys covers anything else you want locked down that isn't secret-named - database.connections, session, feature flags that should never move, etc:

'protected_keys' => [
    'database.connections',
    'session',
],

Entries in both lists support Str::is()-style wildcards (e.g. 'mail.*') and also block anything nested under them ('database.connections' blocks database.connections.mysql.host too). Protection is enforced by snapshotting the real value at each protected path before any source is merged in, and forcing it back afterward - so a source can't get around it by overwriting a shallower ancestor (e.g. setting the whole app key to a single scalar) instead of the exact protected leaf.

dynamic-config:debug also redacts any value whose key looks sensitive (*password*, *secret*, *token*, *key*, *credential*) by default, recursively through nested arrays - pass --reveal to print it anyway.

Extending Drivers

You can easily add your own drivers (e.g., API, Redis, Vault) by creating a class that implements Imran\DynamicConfig\Contracts\ConfigDriver and extending the ConfigManager inside your AppServiceProvider.

use Imran\DynamicConfig\ConfigManager;

public function boot()
{
    $this->app->make(ConfigManager::class)->extend('redis', RedisDriver::class);
}

License

The MIT License (MIT). Please see License File for more information.