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

Config Bundle Laravel Package

2lenet/config-bundle

Symfony bundle that gives your app an easy, flexible configuration system. It ships traits and an abstract repository to build your own config entity, plus a ready-to-use cache manager. A perfect companion to the Crudit bundle.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: the package is not for Laravel. 2lenet/config-bundle is a Symfony bundle — namespace Lle\ConfigBundle, wired through Symfony's DI container, Doctrine ORM (resolve_target_entities), Symfony routing (config/routes.yaml), and Twig. There is no service provider, no facade, no Eloquent, no config publishing. It is also not on Packagist (README tells you to add a manual VCS repository stanza), sits at 1 star / 0 dependents, and the reported "last release: 2026-08-03" is a future date — treat the metadata as unreliable.

So the day-one decision is not "how do I install this," it's "what pattern do I port." What the bundle actually gives you is a database-backed settings table with typed accessors, a cache layer, and a warm-up command. That pattern is worth 2–4 engineer-days to reimplement cleanly, and it's the only sane path.

What to read in the README, in order:

  1. The ConfigTrait + AbstractConfigRepository split — that's the whole architecture.
  2. The "Available methods" list — getBool / getString / getText / getInt plus set* counterparts.
  3. The "Warm-up" section — this is the part most people skip and later regret.

The port, minimally. Three files get you to first use.

// database/migrations/xxxx_create_settings_table.php
Schema::create('settings', function (Blueprint $table) {
    $table->id();
    $table->string('group')->index();
    $table->string('label');
    $table->string('type', 16);          // bool|string|text|int|float|password
    $table->text('value')->nullable();
    $table->timestamps();
    $table->unique(['group', 'label']);  // add tenant_id here if multi-tenant
});
// app/Models/Setting.php
class Setting extends Model
{
    protected $fillable = ['group', 'label', 'type', 'value'];

    public function castValue(): mixed
    {
        return match ($this->type) {
            'bool'  => filter_var($this->value, FILTER_VALIDATE_BOOL),
            'int'   => (int) $this->value,
            'float' => (float) $this->value,
            default => (string) $this->value,   // text, string, password
        };
    }
}
// app/Services/Settings.php
class Settings
{
    private array $memo = [];

    public function getBool(string $group, string $label, bool $default): bool
    {
        return (bool) ($this->load()[$group.'.'.$label] ?? $default);
    }

    public function getString(string $group, string $label, string $default): string
    {
        return (string) ($this->load()[$group.'.'.$label] ?? $default);
    }

    public function setBool(string $group, string $label, bool $value): void
    {
        $this->put($group, $label, 'bool', $value ? '1' : '0');
    }

    private function load(): array
    {
        return $this->memo ??= Cache::rememberForever('settings.all', fn () =>
            Setting::all()
                ->mapWithKeys(fn (Setting $s) => [$s->group.'.'.$s->label => $s->castValue()])
                ->all()
        );
    }

    private function put(string $group, string $label, string $type, ?string $value): void
    {
        Setting::updateOrCreate(
            ['group' => $group, 'label' => $label],
            ['type' => $type, 'value' => $value]
        );
        Cache::forget('settings.all');
        $this->memo = [];
    }
}

First use case: an admin-editable settings screen for things that shouldn't need a deploy — maintenance banner, support email, per-feature toggles, SMTP overrides, pagination sizes. Keep genuinely static values in config/*.php; put here only what a non-developer needs to change at runtime.

Implementation Patterns

Group/label convention. The bundle's group is a namespace, label is the key. Adopt a naming discipline immediately — $s->getBool('billing', 'invoices_enabled', false). Two developers inventing billing.invoices_enabled vs BILLING_INVOICES_ENABLED will cost you an afternoon. Constrain labels to [a-z0-9_]+ and validate on write.

Warm-up is not optional — it is your schema. The bundle's lle:config:warmup command runs on every app start and calls initX() (create-if-absent) rather than setX() (overwrite). Port it verbatim:

class SettingsWarmup implements WarmupInterface
{
    public function warmup(Settings $settings): void
    {
        $settings->initBool('billing', 'invoices_enabled', true);
        $settings->initString('mail', 'support_address', '[email protected]');
        $settings->initInt('ui', 'per_page', 25);
    }
}
// app/Console/Commands/SettingsWarmup.php
public function handle(Settings $settings): int
{
    Setting::firstOrCreate(
        ['group' => $group, 'label' => $label],
        ['type' => $type, 'value' => $value]
    );
    // ...
}

Run it in the deploy pipeline (php artisan settings:warmup), not on every web request, and make it idempotent. This is how a new feature flag ships to production without a migration, and it's the single highest-value idea in the whole package.

Typed accessors, not magic strings. Resist a generic get('group', 'label') returning mixed. The typed API (getBool returns bool, never null) is what keeps call sites free of casts. For missing values, always take an explicit $default argument — never return null and let the caller decide.

Cache invalidation on write, always. The bundle ships a CacheManager precisely because this is the failure mode. In Laravel, use a single aggregate cache key (settings.all) with Cache::forget on write. Avoid Cache::tags() — the file and database drivers don't support tags, and you'll discover that in staging. Note the bundle caches because config is read on nearly every page render; keep the same assumption.

Request-level memoization. The $this->memo in the service above means one cache hit per request, not one per getBool(). Without it, a template rendering 40 settings does 40 Redis round-trips.

Blade helper. The bundle offers {{ get_config_value('type', 'group', 'label', 'default') }}. In Laravel, a plain helper is simpler and more type-safe than a directive:

function setting(string $group, string $label, mixed $default = null): mixed
{
    $s = app(Settings::class);
    return match (true) {
        is_bool($default)  => $s->getBool($group, $label, $default),
        is_int($default)   => $s->getInt($group, $label, $default),
        is_float($default) => $s->getFloat($group, $label, $default),
        default            => $s->getString($group, $label, (string) $default),
    };
}

Inferring the type from the default's PHP type keeps templates honest and mirrors how the Symfony side dispatches on getBool/getInt/etc.

Extending the entity — the bundle's headline flexibility is that you subclass or add fields to Config (the README's Establishment relation is the canonical example). In Laravel this maps to adding tenant_id, updated_by, or description columns to settings and scoping the unique index accordingly. Add tenant_id from the start even if you're single-tenant today; retrofitting it into a cached aggregate is a rewrite.

Admin CRUD. The bundle is designed to pair with Crudit. In Laravel, that's Filament / Nova / Backpack over the settings model, with the type column driving the form component (toggle for bool, textarea for text, number input for int, password input with reveal for password). The bundle's release history is literally this same evolution — 1.4.3 "use a textarea on text config", 1.2.6 "password type". Build the type→component map once.

Invalidation on tenant scoping. If you cache an aggregate, the aggregate must include tenant in the key or in the row identity. See the gotcha below.

Gotchas and Tips

The multi-tenancy cache bug is the one to check first. The README's Establishment example clearly implies multiple config rows per group/label, differing by tenant. If the bundle's CacheManager keys on group/label alone, tenant A's value gets served to tenant B — a data-leak-class defect, silently. The README does not answer this. In your port, make the cache key settings.all.{tenantId} and the unique index (tenant_id, group, label). Do not discover this after launch.

Cache invalidation must be explicit on every write path. If you add a manual "reset settings" route, an artisan command, or a seeder that touches the table directly, it bypasses set*() and leaves stale cache. Centralise every write through the service, or add a model observer on saved/deleted that forgets the key.

Never use set*() in warm-up code. The README is explicit: set*() overwrites defined configurations. If someone runs settings:warmup and it calls setString('mail', 'smtp_host', 'localhost'), production config silently reverts. Use init*() / firstOrCreate exclusively. This is the most likely production incident with this pattern.

The documented signatures don't match reality. getInt($group, $label, string $default): int takes a string default and returns an int — a type inconsistency that suggests the docs weren't compiled. Meanwhile release notes add float (1.3.0), password (1.2.6/1.2.7), and translatable labels (1.4.0/1.4.2) that the README's "supported formats" list of four never mentions. Read the releases, not the README, to learn what the package can actually do.

Password values are stored plaintext. password is a form type (masked input), not encryption — nothing in the README or releases suggests value is encrypted, hashed, or rotated. If you port it, either use Crypt::encryptString() on the value or, better, don't put secrets in this table at all. API keys and SMTP passwords belong in the environment or a secrets manager, not in a table editable from an admin screen with no audit trail.

No audit trail. The package records nothing about who changed what, when, or what the previous value was. For anything a customer can see the effect of (pricing, feature gating, legal text), add an settings_audit table via a model observer — or use updated_by plus a LogsActivity-style package. Retrofitting history is impossible.

"Not set null for text value" (1.4.4) is a real trap. The changelog entry tells you the previous behaviour was writing NULL into text values, breaking NOT NULL-style expectations downstream. In your port, coerce text to '' rather than allowing null in the value column, and pick one convention project-wide.

Packaging and namespace smells. The org is 2lenet but the PHP namespace is Lle\ConfigBundle, the README's composer.json snippet is malformed (a bare {"url": ..., "type": "git"} with no repositories wrapper), and there's no Packagist entry. Combined with 1 star and 0 dependents, assume no upstream support commitment. Do not plan on upstream bug fixes arriving.

Testing the port. Use RefreshDatabase and flush the cache in setUp() — stale settings.all in a cached cache store across tests is a classic phantom-failure source. Assert both the DB row and the post-write cache state; the invalidation path is where bugs live, not the read path.

Extension points worth copying. Two design choices in the bundle are genuinely good and should survive the port: (1) the entity is yours, not the package's — no vendor migration to fight when you need an extra column; (2) the WarmupInterface contract lets any module declare its own defaults, so a package or feature module can register settings without the core knowing. Do the same with a SettingsRegistrar service that modules can push defaults into.

Where the pattern stops paying. This is a settings table, not a feature-flag platform. No percentage rollouts, no per-user targeting, no exposure tracking, no experiment analysis. If you find yourself wanting those, stop extending this and reach for LaunchDarkly/Flagsmith/Unleash. Similarly, values read on every request in a hot path should still live in config/ — the DB round-trip (even cached) is a tax you don't need to pay for things that never change at runtime.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel