> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Package config merging and caching

> Learn from Laravel 13's ServiceProvider implementation how shallow merging differs from recursive replacement, the pitfalls of numerically keyed arrays, and how to maintain packages with the config cache in mind.

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](/en/advanced/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.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );
    }

    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../config/courier.php' => config_path('courier.php'),
        ], 'courier-config');
    }
}
```

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()`.

```bash theme={null}
php artisan vendor:publish --tag=courier-config
```

<Warning>
  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.
</Warning>

## 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.

```php theme={null}
<?php

$defaults = [
    'enabled' => true,
    'transport' => [
        'timeout' => 10,
        'retries' => 3,
    ],
];

$overrides = [
    'transport' => [
        'timeout' => 30,
    ],
];

$config = array_merge($defaults, $overrides);

var_export($config);
```

```text theme={null}
array (
  'enabled' => true,
  'transport' =>
  array (
    'timeout' => 30,
  ),
)
```

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.

```php theme={null}
public function register(): void
{
    $this->replaceConfigRecursivelyFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
```

Using the same `$defaults` and `$overrides` as before produces the following result.

```php theme={null}
$config = array_replace_recursive($defaults, $overrides);

var_export($config);
```

```text theme={null}
array (
  'enabled' => true,
  'transport' =>
  array (
    'timeout' => 30,
    'retries' => 3,
  ),
)
```

| Config contract | Method to choose | Caveat |
| - | - | - |
| Users specify nested arrays in full | `mergeConfigFrom()` | Unspecified keys inside the array are not filled in |
| Users specify only part of nested options | `replaceConfigRecursivelyFrom()` | Numerically keyed lists are also replaced recursively |

<Info>
  `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.
</Info>

### 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.

```php theme={null}
<?php

$defaults = ['channels' => ['mail', 'database']];
$overrides = ['channels' => ['slack']];

$config = array_replace_recursive($defaults, $overrides);

var_export($config['channels']);
```

```text theme={null}
array (
  0 => 'slack',
  1 => 'database',
)
```

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.

```mermaid theme={null}
flowchart TD
    A["php artisan config:cache"] --> B["Delete old config cache"]
    B --> C["Boot a fresh application"]
    C --> D["Load config files<br>Merge in providers"]
    D --> E["Save the entire config repository"]
    E --> F["Later boots use the saved config<br>Both merge methods are skipped"]
```

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.

```bash theme={null}
php artisan config:cache
```

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.

<Warning>
  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.
</Warning>

## 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.

## Related pages

<Columns cols={2}>
  <Card title="Package testing" icon="flask" href="/en/advanced/package-testing">
    Register service providers and verify how your config and services behave.
  </Card>

  <Card title="Version compatibility management" icon="code-branch" href="/en/advanced/package-versioning">
    Connect public API changes to your release policy and ongoing maintenance.
  </Card>
</Columns>

## Primary sources

* [Laravel documentation: Package configuration](https://github.com/laravel/docs/blob/13.x/packages.md#configuration)
* [ServiceProvider: merge and publish implementation](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Support/ServiceProvider.php)
* [ConfigCacheCommand: generating the config cache](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ConfigCacheCommand.php)
* [LoadConfiguration: loading config](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php)
* [ConfigClearCommand: clearing the config cache](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ConfigClearCommand.php)


## Related topics

- [Laravel Package Development](/en/advanced/package-development.md)
- [Advanced Topics](/en/advanced/index.md)
- [Package auto-discovery internals](/en/advanced/package-discovery.md)
- [Configuration](/en/configuration.md)
- [Laravel AI SDK](/en/ai-sdk.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.