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

Filament Db Config Laravel Package

inerba/filament-db-config

DB Config is a lightweight Filament plugin offering a database-backed key/value store for application settings and editable content. Manage config values and content directly from your Filament admin panel without touching files or redeploying.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm compatibility first. The package targets Laravel 12 + Filament 4/5 and needs a JSON-capable column (MySQL 5.7+, MariaDB 10.2.7+, Postgres, recent SQLite). On a Laravel 10/11 or Filament 3 app, stop here — this isn't the plugin for you. Check the installed versions before composer require:

composer show filament/filament laravel/framework | grep versions

2. Install and create the table (no migration to write yourself):

composer require inerba/filament-db-config
php artisan vendor:publish --tag="db-config-migrations"
php artisan migrate

Only publish the config if you actually need to change something — see Gotchas for why you should decide on the table name now rather than later:

php artisan vendor:publish --tag="db-config-config"   # config/db-config.php

3. Scaffold, don't hand-write. Everything in this package hangs off one Artisan generator:

php artisan make:db-config Website

This produces app/Filament/Pages/WebsiteSettings.php extending Inerba\DbConfig\AbstractPageSettings. The "Settings" suffix and singularization are automatic — Website in, WebsiteSettings out. The generator refuses to overwrite existing files, so re-running it is safe but useless.

4. Where to look first, in this order:

File Why
The generated page class It shows whether your installed version expects content(Schema $schema) or form(Form $form) — the README shows both, and they belong to different Filament majors
settingName() in that class This is the group prefix for every key on the page. If it says website-settings and your helper calls db_config('website.site_name'), nothing resolves
config/db-config.php Table name, cache prefix, cache TTL
The db_config table Two-ish columns of JSON — useful to eyeball when a value "isn't saving"

5. First use case — the canonical one. Site-wide settings that a non-technical editor owns:

// app/Filament/Pages/WebsiteSettings.php
protected function settingName(): string
{
    return 'website';
}

public function content(Schema $schema): Schema
{
    return $schema
        ->components([
            TextInput::make('site_name')->required(),
            TextInput::make('contact_email')->email(),
            Toggle::make('maintenance_mode'),
        ])
        ->statePath('data');
}

The page ships with a header Save action that persists each top-level key as website.{key} — you write no save logic. Consume it:

<h1>@db_config('website.site_name', 'Acme')</h1>

That's the whole loop: define fields → editor saves in the panel → read anywhere.


Implementation Patterns

Scaffold-then-edit is the workflow. make:db-config → edit the form → done. The plugin's own skill file is explicit that hand-writing page classes is a supported non-pattern; the abstract base carries the mount/save lifecycle you'd otherwise reimplement badly. Likewise, skip writing feature tests for settings pages — the value under test is a form component you didn't write.

Namespace keys by page, not by type. The first path segment is your group, and DbConfig::getGroup() is your bulk-read. One page = one group:

db_config('website.site_name')
db_config('seo.meta_title')
db_config('checkout.button_label')

Avoid db_config('site_name') (no group). It technically resolves, but you lose getGroup(), you lose per-page isolation, and the page's Save action only writes under its own settingName() — so an ungrouped key can never be edited from the UI.

Read the group once in loops and views. Every db_config() call on a cold cache goes to the store; on a warm cache it's cheap but you're still doing prefix work per key. For a block of related settings, prefer one call:

$site = \Inerba\DbConfig\DbConfig::getGroup('website');
// ['site_name' => ..., 'contact_email' => ..., 'maintenance_mode' => ...]

Defaults live in two places, and they're not the same thing. getDefaultData() on the page pre-fills the form; the second argument to db_config() is the runtime fallback. Keep them in sync by defining the array once as a constant or config value and reusing it:

public function getDefaultData(): array
{
    return ['site_name' => 'Acme', 'maintenance_mode' => false];
}

// elsewhere
db_config('website.maintenance_mode', false);

getDefaultData() is also your seed path: it's the cheapest way to give a fresh environment sane values before anyone opens the panel.

Write programmatically for onboarding, imports, and feature flags. DbConfig::set() is the only mutation API worth using, because it's the one that invalidates the cache:

use Inerba\DbConfig\DbConfig;

DbConfig::set('website.site_name', $tenant->name);

Good fits: seeding defaults in a deploy step, flipping a kill-switch from an admin action outside the settings page, migrating legacy values out of config/*.php on first boot.

Use the widget base for dashboard-hosted settings. AbstractWidgetSettings gives you the same save lifecycle inside a Filament widget — handy when the editor's mental model is "a card on my dashboard" rather than "a page in the nav." Same contract: settingName(), form(Schema $schema), ->statePath('data'), plus getDefaultData().

Pick fields for the editor, not for the schema. Any Filament field or layout component works, including third-party ones — that's the actual differentiator versus writing a bespoke settings resource. Rich editors, repeaters, and layout sections are all fair game. For richer content (homepage hero, landing blocks), model the block as one key rather than exploding it into twelve scalar keys: one Repeater stored as a JSON array is easier to reorder and version than twelve keys that must stay in lockstep.

Boot-time reads: use safe_db_config(). In a service provider, middleware, or anything that can run before migrate has ever executed, db_config() will throw on a missing table and take the whole request down. safe_db_config() swallows the exception and returns your default:

// AppServiceProvider::boot()
config(['app.name' => safe_db_config('website.site_name', config('app.name'))]);

Inject into config at boot, but do it narrowly. Bridging one or two values into config() in a provider is a common, sane pattern — it lets packages that read config() directly participate. Don't bridge twenty; you've just recreated a config file with extra latency.


Gotchas and Tips

The README contradicts itself on the method signature. "Getting Started" shows form(Form $form) (Filament 3 style); "Advanced Usage" shows content(Schema $schema) (Filament 4/5). On Filament 4/5 the base class calls content() — write a form() and your fields may simply never render, with no error. Open the generated file and copy its signature rather than trusting either snippet.

The skill file contradicts the README on the generator's arguments. README documents make:db-config website admin (name + panel); the bundled skill says never pass a second argument. If the two-argument form fails, run the command bare and answer the interactive prompt — that path is documented as supported in both.

->statePath('data') is not optional. Omit it and the header Save action iterates an empty state array and writes nothing — silently. Same failure mode if the $data property is renamed. This is the single most common "it doesn't save" cause.

Only top-level keys are written. The Save action loops the page state and calls set("{group}.{key}", $value) per top-level entry. A nested array is stored as an array under one key. So db_config('website.hero.title') is only reliable if the package's dot-notation resolution descends into stored arrays — verify with a one-liner in tinker before you build twelve Blade partials on the assumption:

php artisan tinker --execute="dump(db_config('website.hero'));"

If it doesn't descend, read the parent and index in PHP.

Cache TTL defaults to forever, which makes out-of-band writes invisible. cache.ttl is null by default. Anything that touches the db_config table without going through DbConfig::set() — a raw SQL fix in production, an import script, a second service — leaves stale values in the cache indefinitely. If you ever hand-edit the table, follow it with a cache flush scoped to your prefix, or set a finite TTL:

'cache' => ['prefix' => 'my-app-settings', 'ttl' => 60],

Decide the table name before you migrate. table_name lives in the config file, which the README tells you to publish before running the migration. Change it afterwards and you're renaming a table. Also keep cache.prefix distinct per app if you share a Redis instance — the default prefix is generic enough to collide.

There's no authorization on the generated page. It's a normal Filament page, so panel access is the only gate — meaning every user who can log into the panel can edit your site's settings. Gate it explicitly (canAccess() on the page, or a policy) if the panel has non-admin users.

No tenancy, no revisions, no audit trail. The store is global. Multi-tenant setups must encode the tenant into the group name (website.acme.site_name) and manage lifecycle themselves; multilingual content needs the same treatment. And since there's no history, "who changed the homepage hero last Tuesday" is unanswerable out of the box — if that matters, wrap all writes in a thin service of your own that logs before delegating to DbConfig::set(). That wrapper is also your cleanest extension point for anything the package doesn't do.

File uploads store a path, not a URL. FileUpload will happily save to the key, but rendering it needs Storage::disk(...)->url(...) at the call site, and the disk/directory config lives on the field — so changing the disk later orphans existing values. Keep the disk name in the key's group and don't move it.

Test with the array cache driver. If your test suite uses Redis or the database cache, values written by one test leak into the next via the settings cache. Set CACHE_STORE=array in phpunit.xml and use RefreshDatabase; seed any needed values with DbConfig::set() in the test that needs them.

Treat this as a small dependency. 37 stars, zero dependents, single-maintainer, released recently. That's fine for a settings table — the blast radius is small and the table is plain JSON you could read yourself — but pin the major version in composer.json, and if you're building anything load-bearing on top of it, route access through one small internal helper (app_settings()) so swapping the package out later is a one-file change rather than a grep across the codebase.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log