Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dynamically Configurable App Bundle Laravel Package

modera/dynamically-configurable-app-bundle

A Symfony bundle from Modera that lets application configuration be defined and changed at runtime instead of being hard-coded in files. It provides the infrastructure to store, load, and apply dynamic settings, making apps easier to adapt per environment or tenant without redeploying.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: modera/dynamically-configurable-app-bundle is a Symfony Bundle — it hooks Kernel::registerBundles() / Kernel::boot(), which Laravel doesn't have. Last release was 2016-06-13, it's READ-ONLY, has 1 star and 0 dependents. Do not composer require it — on PHP 8 it will fail platform constraints and pull nothing you need. You are here to steal the pattern, not the dependency.

Where to look first (skim, don't read deeply):

  1. DependencyInjection/ — the extension class + Configuration tree. This is where the bundle declares which values can be overridden at boot and what their defaults/validation look like. This is the part that maps 1:1 to a Laravel settings service.
  2. The *Kernel / boot hook classes — how it swaps config before the container is compiled. This is the equivalent of code that runs in bootstrap/app.php before $app->boot().
  3. Any persistence classes — the "runtime-editable store". In Laravel this becomes a DB table + a repository, or a JSON blob in Redis.

Your first concrete use case (the Laravel version of what this bundle does):

You want per-tenant or per-environment behaviour decided before the service container resolves anything — e.g. different storage driver, queue connection, or feature-flagged providers per customer.

Minimal Laravel-first step, in bootstrap/app.php:

use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withProviders() // your baseline providers
    ->withMiddleware()
    ->withExceptions()
    ->create();

Then override config before boot in a very early bootstrap provider (see next section). If you were hoping the bundle's "runtime configuration" feature replaces .env + config/*.php — it doesn't need to. config() overrides before $app->boot(), plus conditional service providers, cover ~95% of what it offers, with a maintainer, documentation, and security patches.

Do not use this package to solve: plain settings/feature flags (use laravel/pennant or a settings table read through a cached service), per-environment config (.env.{environment} files), or swapping implementations per tenant (conditional bind()/singleton() in a provider).

Implementation Patterns

Assume you are re-implementing the intent. Here are the day-to-day patterns, ordered from cheapest to most invasive.

1. Runtime-editable settings via a cached repository (the "flexible parameter definitions" feature)

Declare typed, validated, defaulted settings — mirrored from the bundle's Configuration tree — and resolve them through one service:

// config/settings.php — the authoritative schema (defaults + types)
return [
    'mail.from' => ['env' => 'MAIL_FROM_ADDRESS', 'default' => '[email protected]', 'type' => 'string'],
    'queue.driver' => ['env' => 'QUEUE_CONNECTION', 'default' => 'redis', 'type' => 'string'],
    'features.new_billing' => ['env' => 'FEATURE_NEW_BILLING', 'default' => false, 'type' => 'bool'],
];
// app/Settings/SettingsRepository.php
class SettingsRepository
{
    public function __construct(private DatabaseStore $db, private array $schema) {}

    public function get(string $key): mixed
    {
        $definition = $this->schema[$key] ?? throw new RuntimeException("Unknown setting: {$key}");

        $value = $this->db->get($key)
            ?? env($definition['env'])
            ?? $definition['default'];

        return match ($definition['type']) {
            'bool'    => filter_var($value, FILTER_VALIDATE_BOOLEAN),
            'int'     => (int) $value,
            'json'    => json_decode($value, true, flags: JSON_THROW_ON_ERROR),
            default   => (string) $value,
        };
    }
}

Cache it (Cache::rememberForever('settings:all', ...)) and bust the cache in the write path. This is the "admin-friendly" part: expose SettingsRepository::get/set in your Filament/Nova/admin controller; validation comes free from the $schema map.

2. Per-tenant / per-environment container configuration

The bundle's real trick is deciding how the app is assembled — the Laravel equivalent is conditional provider registration:

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $tenant = $this->app->make(TenantResolver::class)->current();

    if ($tenant?->usesS3()) {
        $this->app->singleton(FilesystemDriver::class, S3Driver::class);
    } else {
        $this->app->singleton(FilesystemDriver::class, LocalDriver::class);
    }
}

Do this in a provider inside the fixed bootstrap/providers.php list (L11+) — not in a bootstrap file with side effects, and not by mutating config/app.php at runtime.

3. Environment-conditional bootstrapping ($app->bootstrapWith([...]))

This is the truest analog to the Symfony bundle — pick the bootstrapper stack at runtime. It is powerful and dangerous; only reach for it when the previous two patterns fail.

// bootstrap/app.php
$app = Application::configure(basePath: dirname(__DIR__)) /* ... */ ->create();

if (env('APP_BOOTSTRAP_PROFILE') === 'worker') {
    $app->bootstrapWith([
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
        // note: no RegisterFacades/Route registration if you want CLI-only
    ]);
}

return $app;

Use case: single codebase, different boot profile for web / queue worker / CLI / tenant-isolated job. Always gate this on an env var with a safe default; never derive it from user input.

4. Feature flags resolved at boot

Overlap between "dynamic config" and "feature flags" is where this bundle was heading. Modern answer:

// app/Providers/FeatureServiceProvider.php
public function register(): void
{
    $flags = $this->app->make(FeatureFlagReader::class)->all();      // reads cached settings

    if ($flags['new_billing']) {
        $this->app->bind(BillingEngine::class, NewBillingEngine::class);
    }
}

The rule of thumb: toggle bindings at registration time, toggle behaviour at runtime. Binding a class based on a flag means the flag is effectively read-only per request — a mid-request change will not swap an already-resolved singleton.

5. Integration with config caching

Whatever you do, ensure your runtime settings layer survives php artisan config:cache. That means: the settings repository must be a service, not a config file you edit in place; and any boot-time branch must be derived from env vars that are baked into the image, not from DB reads that require a live connection before the DB provider registers.

Ordering matters:

LoadEnvironmentVariables → LoadConfiguration → your settings provider
   → DB-backed settings (OK here, connection is available)
   → config overrides via config(['x' => ...])
   → RegisterProviders/BootProviders
   → conditional bindings

Gotchas and Tips

Blast radius. Bootstrapping code sits beneath every request, queue job, scheduled command, and test. A bug here is not a 500 on one route — it's "the app starts but wiring is wrong" across all of them. Treat every change to bootstrap/app.php and early providers as a production-adjacent change: PR review, CI matrix coverage of all profiles, and a rollback plan that doesn't require a rebuild.

Config caching defeats runtime config — by design. If settings must be changed without a deploy, they cannot live in config/*.php. If they can tolerate a deploy, they don't need a runtime store. Pick one. Mixing the two is the #1 source of "it works in staging, fails in prod" reports with this pattern.

Silent misconfiguration. An env var differing by one character (QUEUE_CONNECTION=redis with a trailing space) causes the boot branch to take the wrong path and the app still starts. Defend in depth: (a) validate the boot profile against a whitelist at the top of bootstrap/app.php, (b) log the resolved profile once per process at info level including a hash of the resolved settings, (c) add an artisan command php artisan app:boot-profile that prints the active profile and each resolved setting.

Non-reproducible environments. If local, CI, staging, and prod take different code paths, works on my machine becomes structural. Add a CI job that boots the app under each profile (web, worker, cli) and runs a smoke test — php artisan about with each APP_BOOTSTRAP_PROFILE is a cheap start.

Cache poisoning. If a boot-time decision is cached (settings, feature flags, resolved providers), a bad boot can persist the bad decision past the process that made it. Never write to Cache from a bootstrap path; only read. Provide an explicit php artisan settings:refresh to clear.

Ordering pitfalls. TenantResolver often needs the DB; the DB needs config; config needs env. If you register a tenant-aware provider that reads settings from the DB, you must ensure RegisterProviders runs after LoadConfiguration, or you get Target [db] is not instantiable during console commands. Test your providers with php artisan tinker and php artisan config:cache — both exercise different orders than web requests.

Tests will hide the bug. phpunit boots a fresh app per test with APP_ENV=testing and typically no DB write, so tenant-aware bindings, cache poisoning, and profile branching are all unexercised. Write one feature test that explicitly sets APP_BOOTSTRAP_PROFILE and exercises each branch — or use Pest arch tests to assert the right class is bound:

it('binds S3 driver for s3 tenants', function () {
    $this->app->instance(TenantResolver::class, new FakeTenantResolver(usesS3: true));
    $this->app->register(AppServiceProvider::class, force: true);
    expect($this->app->make(FilesystemDriver::class))->toBeInstanceOf(S3Driver::class);
});

Extension points you do want to copy from the bundle. Its schema-driven Configuration tree is genuinely nice: keys map to types, defaults, and validation, and the same schema drives both the admin form and the runtime reader. Do the same in config/settings.php + SettingsRepository — one source of truth for what can be changed, what its type is, and what default applies. Then the admin UI is a generic form over array_keys(config('settings')).

Debugging tips.

  • php artisan about shows resolved config, drivers, and env — your first stop when a boot branch is suspected.
  • php artisan config:show (L11+) narrows it to a single file.
  • For a "wrongly wired" symptom, dump get_class() of the suspicious binding in a route/tinker rather than reading config: config lies, the container doesn't.
  • If a provider registration order matters, php artisan event:list and php artisan route:list --verbose both fail loudly with a stack trace that shows provider ordering — use them as canaries.
  • Add a temporary \Log::info('boot', [...]) at the top of bootstrap/app.php if you need to prove the file is even being executed (opcache / path issues).

Do not let anyone extend this bundle by forking it into app/. If the requirement is real, the ~2–3 engineer-days estimate for a native implementation is cheaper than owning 2016-era boot code forever. Every one of the patterns above is idiomatic Laravel and can be covered by your existing test suite — the Symfony bundle cannot.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin