Skip to main content
Adding options to your package’s config does not automatically write new entries into config files that users have already published. To fill in default values without breaking published configs, you need to design both your array merge strategy and how you handle the config cache. This page walks through Laravel 13’s ServiceProvider and explains how to maintain config as part of your package’s public API. It assumes you are familiar with Laravel package development, and the implementation was checked against laravel/framework v13.34.0.

Publishing and merging are separate operations

publishes() registers a source and a destination for copying. The user’s config directory does not change until vendor:publish copies the file. In contrast, mergeConfigFrom() updates the config repository at boot time and never modifies the file itself.
Users publish the config file only when they need it. Even without publishing, a normal boot with no cache uses the default values through the merge in register().
If your upgrade instructions tell users to republish the config file with --force, their edits will be overwritten. When you are only adding new config options, prefer filling in default values and documenting what changed.

mergeConfigFrom() merges only the top level

ServiceProvider::mergeConfigFrom() calls array_merge() with the package config first and the application’s existing config second. For the same string key, the application’s value wins. The following example reproduces the framework’s merge using plain arrays.
The top-level enabled is filled in, but the entire transport array is replaced. transport.retries does not survive. The key point: if a user has already published an older transport array, any new keys you add to that array will not be filled in.

Fill in nested config with replaceConfigRecursivelyFrom()

Laravel 13’s ServiceProvider also has a protected method, replaceConfigRecursivelyFrom(). It calls array_replace_recursive() with the same argument order. If you want a config API where users can override nested string keys individually, change your provider’s register() as follows. You do not need to combine it with the earlier mergeConfigFrom() call for the same config key.
Using the same $defaults and $overrides as before produces the following result.
replaceConfigRecursivelyFrom() is a method that exists in the Laravel 13 source code examined on this page. Keep it distinct from mergeConfigFrom(), which the official package development documentation covers, and check the corresponding framework implementation before using it.

Numerically keyed lists are not replaced as a whole

Recursive replacement does not mean “swap the whole array for the user’s value.” Even with numeric keys, it replaces values at matching keys and keeps any keys the user did not specify.
Even if the user specifies only ['slack'], database remains. Passing ['channels' => []] does not empty the default list either. Be especially careful with settings where specifying the whole list matters, such as notification channels or middleware. If your config has settings like this, start by reconsidering its structure. For example, split lists and partially overridable associative arrays into separate top-level keys and use a shallow merge. Changing the merge strategy in a package you have already released changes behavior for the same config file, so do not treat it as a simple implementation swap.

The config cache stores merged values

Both methods skip merging when the application implements CachesConfiguration and configurationIsCached() returns true. In a typical Laravel application, this is the case whenever the app boots with a config cache present. ConfigCacheCommand deletes the old config cache, boots a fresh application, and retrieves the entire config repository. Provider configs are merged during that boot, and the result is saved to the cache file. On subsequent boots, LoadConfiguration loads those values. As a result, when you update a package and its default values or merge strategy change, applications that keep using an old cache will not see the change. In deployments that use the config cache, rebuild it with the updated code.
To go back to reading from files during development, use php artisan config:clear. Do not choose your own cache location; let Laravel’s commands manage it.
Do not define closures in config files. They cannot be serialized correctly by config:cache. If you need to pass a callback, put something like a class name in the config and register the actual service in your provider.

Checks before releasing config changes

In your package tests, treat configs left over from older versions as input, not just unpublished configs.
  • Required default values are available even when the config is not published.
  • With an older published config, the user’s values take precedence and new options are filled in as designed.
  • The override contract does not change for nested arrays, numerically keyed lists, and empty arrays.
  • config:cache succeeds in a consuming application, and a separate boot using the cache produces the same config.
Document the default values of new keys and any required cache rebuild in your release notes. When removing or renaming keys or changing the merge strategy, factor in compatibility with users’ published configs.

Package testing

Register service providers and verify how your config and services behave.

Version compatibility management

Connect public API changes to your release policy and ongoing maintenance.

Primary sources

Last modified on October 1, 2026