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

Composite Config Laravel Package

cartalyst/composite-config

Composite Config is a Cartalyst package for merging and layering configuration from multiple sources into a single composite config repository, letting PHP applications organize, override, and resolve settings cleanly. A small utility aimed at developers who need flexible config composition.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Verify it resolves before you plan around it. Last release is 5.1.0 (2020-12-23, "Add PHP 8 support"), and Cartalyst version lines historically track the Laravel major they target. Check the illuminate/* and php constraints inside the package's own composer.json before assuming it installs on Laravel 10/11/12. If composer require refuses, run composer why-not cartalyst/composite-config 5.1.* — a stale config.platform.php in your root composer.json is the usual culprit.
  • Clear the license first. It is flagged NOASSERTION. Cartalyst ships a BSD-3 grant with a supplemental Cartalyst clause. Get that read and approved before it lands in a production composer.json, because it is materially different from MIT and most automated license scanners will flag it.
  • Read the source in this order: the service provider's register() (that is where config gets rebound in the container — it is the documentation), the repository class that decorates Illuminate\Config\Repository, the publishable migration/table schema, then the published config file. There is effectively no README.
  • Register the provider manually. Do not rely on package auto-discovery for a 2020-era Cartalyst package. Add it to config/app.php (or bootstrap/providers.php) explicitly, and place it before any provider that reads config() inside its own register().
  • Pick a deliberately boring first use case. Take one non-secret, non-boot-critical key — app.name, a pagination size, a feature default — publish the migration, run it, insert one override row, and read it back. Do not touch auth, database, mail, or queue config until a single read confirms the binding actually swapped.
composer require cartalyst/composite-config:5.1.*
php artisan vendor:publish --provider="Cartalyst\CompositeConfig\CompositeConfigServiceProvider"
php artisan migrate
php artisan tinker
>>> get_class(app('config'))   # must NOT be Illuminate\Config\Repository
>>> config('app.name')          # must reflect your row

That get_class() check is the one-line smoke test you will run for the rest of the project's life. If it still says Illuminate\Config\Repository, the provider did not register or registered too late and nothing else matters yet.

Implementation Patterns

  • Treat the package as invisible. The entire value proposition is that config('x') keeps working and callers never know whether the value came from a file or a row. Keep config() / config([...]) in application code and never call CompositeConfig::get() in a controller — that couples every call site to a zero-star dependency. If you ever need to swap the implementation, the goal is to change one container binding, not grep the codebase.
  • Write the precedence contract down in a comment wherever the config layer is registered, so it survives staff turnover: files hold defaults and structure (version-controlled, reviewed), the database holds per-environment or per-tenant deltas (deployable without a release), runtime config([...]) holds request-scoped overrides (tests, jobs, one-off scripts). Keep the DB layer sparse — only keys that genuinely differ. A sparse table means a missing table, an empty table, or a broken query is a survivable state that silently falls back to file values.
  • Guard the boot path. The repository touches the database while the container is still booting — before the exception handler is fully wired, in some configurations before the DB connection itself is usable. Wrap the read in a Schema::hasTable() check plus a try/catch that falls back to file values:
try {
    $overrides = Cache::rememberForever("config.overrides.{$scope}", function () {
        if (! Schema::hasTable('config_overrides')) {
            return [];
        }
        return DB::table('config_overrides')->pluck('value', 'key')->all();
    });
} catch (\Throwable $e) {
    report($e);
    $overrides = [];
}

Without this, a fresh clone, a CI job with no database service, or php artisan migrate on an empty schema hard-fails before your app can render an error page — and the stack trace will point at the config layer, not at the actual cause.

  • Never persist through config([...]). In most implementations set() mutates the in-memory array only. The classic bug is an admin settings screen that "saves," returns success, and reverts on the next request. Route all writes through your own small service or repository class that hits the table directly and then Cache::forget()s the override key.
  • Cache the override set, not individual keys. Lazy per-key loading turns a boot-time layer into an N+1 query against the config table on every cold request. Load the whole table once, key it by tenant/connection/scope, rememberForever(), and invalidate on write. One query per process boot is the budget; anything more is a regression you will see in APM as a startup spike.
  • Order tenant resolution before the config repo's first read. This is the hardest wiring problem in the package, because tenant resolution usually wants to read config (connection strings, domains) while config wants the tenant to scope its query. The clean split: keep tenant identity (domains, connection names) in env/files, and tenant settings (branding, limits, toggles) in the database. Pass the resolved tenant identifier through constructor injection into your repository decorator rather than mutating global config mid-request.
  • Import existing values as data before you switch the reader on. Write a one-off seeder or data migration that copies the values currently hard-coded in config/*.php into the override table, so the file values become defaults rather than the live source. Diff the resulting rows against the files before shipping — that diff is your rollout checklist and your rollback plan.
  • Keep types intact on the way out. Use a JSON column or an explicit cast/serializer in the repository so booleans, integers, and arrays survive the round trip. A text column plus json_decode on read is the low-effort version; a json column plus $casts is the cleaner one.
  • Testing. In unit tests, bind a plain new \Illuminate\Config\Repository([...]) in setUp() and never let the composite layer touch a database — the tests should assert your merge logic, not Laravel's. In feature tests, use RefreshDatabase plus the Schema::hasTable() guard, because the boot-time read fires before migrations run and will otherwise throw against an empty SQLite file.

Gotchas and Tips

  • You are the maintainer. 0 stars, 0 dependents, one release in 2020, an unresolved license string, no issue tracker activity. Pin the exact version (5.1.0, not ^5.1) and disable automated dependency updates for it. Better still, vendor a copy of the source into your repo (path repository or a copied namespace) so a future Composer resolve cannot silently take it away or patch it under you.
  • Seriously price the build alternative. The described behavior is a decorator around Illuminate\Contracts\Config\Repository: read the DB layer, fall through to the file layer, honor dot-notation and defaults. That is a few hundred lines for a competent Laravel engineer, fully testable, with no license question. Given the support profile here, that is often the correct decision — and reading this package's source is a fine way to spec it.
  • config:cache is the biggest behavioural trap. A cached config file is a flat PHP array baked from the file repository. Depending on how the provider hooks in, your DB overrides either get frozen into that file at cache time (stale after every settings change until you re-run config:cache) or get bypassed entirely (the cache gives you nothing and you pay the cost for free). Test both states explicitly — with and without bootstrap/cache/config.php present — before you ship, and write down which one is true in your environment.
  • Provider ordering decides who sees what. Any package that reads config() inside its own register() sees the raw file repository if it registers before this provider. Symptom to recognize: the value is correct in tinker but wrong in the booted app. Confirm by adding a temporary dd(config('your.key')) inside the competing provider's register().
  • Strings lie about their type. MySQL hands back "1", "0", and "null" as strings. config('feature.enabled') returning "0" is truthy in PHP. This is the single most common post-launch bug in DB-backed config; cast at the repository boundary and add one test per boolean/array key.
  • Decide what null means, and test it. Whether a row with a null value shadows the file value or falls through to it depends on the implementation. Laravel's get($key, $default) only applies the default when the key is genuinely absent, so a shadowing null will surprise you. One explicit test settles it forever.
  • Long-running processes do not see your changes. Octane workers, queue:work, horizon, and swoole read config once at boot. Editing a row has no effect on a running worker. Plan a reload path (php artisan octane:reload, queue restart signal) or accept and document "restart on settings change" — otherwise the on-call debug session is brutal and the bug report will be "settings don't save."
  • Never move boot-critical or credential config into the table. APP_KEY, DB_*, MAIL_*, cache/queue connection strings, and anything read before the database connection exists must stay in .env or files. A config override that depends on the database cannot configure the database — that is a boot loop, not an override.
  • If you store secrets, encrypt them. Database-stored API keys need Laravel's encrypted cast, an authorized admin surface, and rows you never hand-edit. Database-backed config is not a secrets vault; treat rotation and audit as out of scope here and use a real vault when they matter.
  • Know the scope boundary against feature flags. If the requirement is really "turn feature X on for 10% of tenants" with targeting, audit, and instant rollback, this is the wrong tool — Laravel Pennant or a hosted flag service is the answer. Config layering answers "what are the values," not "who gets the feature." Mis-scoping this is expensive in both directions.
  • Debugging ladder, in order: get_class(app('config')) to confirm the swap → Schema::hasTable('config_overrides') to confirm the table exists → DB::table('config_overrides')->get() to confirm the data → toggle config:cache on and off to isolate caching → check storage/logs/laravel.log for the real exception underneath the one you swallowed in the boot guard.
  • Extension points to prefer: decorate the Repository contract with your own class, keep the schema in your own migrations rather than the package's, and route all writes through your own service. When this package inevitably needs replacing, you swap one binding and one class rather than hunting call sites across the application.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels