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

# Registering and re-resolving package Storage drivers

> Examine the Laravel 13 FilesystemManager to learn how Storage::extend() registration works, how disk instances are retained, how to re-resolve disks after configuration changes, and how to verify drivers without relying on fake.

When a package provides storage features, keep **registering how a driver is created** separate from **creating the user's disks**. If you design the package so that booting the provider alone never opens external connections or touches files, you are less likely to break the boot process in environments that don't use the feature.

This page is based on the Laravel Framework `v13.37.0` implementation. For basic usage, see [File storage](/en/filesystem). For how providers are loaded, see [Package discovery](/en/advanced/package-discovery).

## Separate driver names from disk names

The first argument to `Storage::extend()` is a **driver name**. The argument to `Storage::disk()`, on the other hand, is a **disk name** defined in `filesystems.disks`. You can define multiple disks that use the same driver.

| Element | Example on this page | Role |
| - | - | - |
| Driver name | `acme-archive-local` | Identifier of the callback that creates disks |
| Disk name | `reports`, `exports` | Identifier of the configuration and the created instance |
| Disk configuration | `root`, `throw`, and so on | Values passed to the callback when that disk is created |

`FilesystemManager::resolve()` looks for a custom creator matching the configured `driver` and, if one exists, calls it before any built-in driver. Using `local` or `s3` as an extension name replaces the existing creator, so choose a name specific to your package.

## Register only the creation logic in the provider

The following minimal example reuses Laravel's standard local driver creator to illustrate the registration path. If you only need local storage, the regular `local` driver is sufficient. When you provide a custom backend, replace this with logic that builds a Flysystem adapter and wraps it in Laravel's `FilesystemAdapter`, as in the official documentation example.

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

namespace Acme\Archive;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;

class ArchiveServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Storage::extend('acme-archive-local', static function (
            Application $app,
            array $config,
        ): FilesystemAdapter {
            if (! is_string($config['root'] ?? null) || $config['root'] === '') {
                throw new InvalidArgumentException('Archive disk requires a root.');
            }

            return $app['filesystem']->createLocalDriver($config);
        });
    }
}
```

Register this provider [in the package the usual way](/en/advanced/package-discovery). Don't restrict registration with `runningInConsole()`, so the driver is available to HTTP requests, Artisan, and queue workers. Also, don't make a provider whose only job is registering the extension a [deferred provider](/en/advanced/deferred-provider).

The callback receives the application and **the entire configuration of the disk being resolved**. Validate the configuration and create clients inside the callback, and don't perform file operations in `boot()`. The `createLocalDriver()` call in the example may create a directory when the disk is created, so keeping registration time and creation time separate matters.

<Tip>
  `FilesystemManager::extend()` binds `$this` in a regular closure to the Manager. Rather than assuming you can use the provider's `$this`, use a `static` closure and the `$app` argument as in this example to keep dependencies explicit.
</Tip>

Users add the configuration to the `disks` array in `config/filesystems.php`.

```php theme={null}
'reports' => [
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/reports'),
    'throw' => true,
],
'exports' => [
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/exports'),
    'throw' => true,
],
```

Calling code specifies the disk name, not the driver name.

```php theme={null}
use Illuminate\Support\Facades\Storage;

Storage::disk('reports')->put('summary.txt', 'Monthly summary');
```

<Info>
  This example shows only the creation path for local storage. Serving files through `serve` and providing temporary URLs are out of scope. If your custom adapter supports URL generation, visibility, read-only mode, or similar features, check each implementation and any required dependency packages. Don't assume a feature is enabled just because you pass a configuration key.
</Info>

## Named disks are retained after creation

`disk('reports')` doesn't call the callback every time. The Manager's `get()` prefers an already created instance in its internal `$disks` array. Only when the disk hasn't been created yet does it go through `resolve()`, and the result is retained under the disk name as the key.

```mermaid theme={null}
flowchart TD
    A["Provider boot()"] --> B["Register creator with extend()"]
    B --> C["Storage::disk('reports')"]
    C --> D{"Has reports been created?"}
    D -->|Yes| E["Return the retained instance"]
    D -->|No| F["Read driver and pass config to the creator"]
    F --> G["Retain the result as reports"]
    G --> E
```

As a result, doing either of the following in the same process doesn't automatically update an already created disk:

* Changing `filesystems.disks.reports` with `config()`.
* Calling `Storage::extend()` again for the same driver name.

`FilesystemServiceProvider` also registers the Manager itself as a singleton. In long-running processes, don't assume disks are always recreated for each request or job.

## Re-resolve only the disks you need after configuration changes

When you change configuration and verify the result within the same process, treat updating the configuration and removing the retained disk as separate steps.

```php theme={null}
use Illuminate\Support\Facades\Storage;

$before = Storage::disk('reports');

config(['filesystems.disks.reports.root' => storage_path('app/private/reports-v2')]);
Storage::forgetDisk('reports');

$after = Storage::disk('reports');
```

`forgetDisk()` removes the given name from the Manager's retention array. The next `disk()` call reads the current configuration again. In the version examined, `purge()` is also implemented as removing the given name. It doesn't call any connection-closing method on a custom backend.

<Warning>
  References held by calling code, such as `$before`, are not replaced. The same applies when a singleton service holds an old disk. Don't use `forgetDisk()` as a mechanism to update every reference or other processes all at once.
</Warning>

For configuration changes and package updates in production, rebuild the configuration cache as part of your normal deployment and restart long-running workers as well. The configuration cache and the in-process disk retention array are separate things.

### Use build for temporary configuration

If you don't want to change shared named configuration, you can use an [on-demand disk](/en/filesystem).

```php theme={null}
use Illuminate\Support\Facades\Storage;

$disk = Storage::build([
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/one-off-export'),
    'throw' => true,
]);
```

`build()` calls `resolve()` directly with the given configuration and doesn't add the result to the named disk retention array. The custom driver still needs to be registered, but each call goes through the creator. Even when you use dynamic storage locations, don't design the system so that users can freely choose storage locations or connection targets from their input.

## Test the creation path separately from fake

`Storage::fake('reports')` uses the Manager's `set()` to replace that disk with a local disk for testing. It's convenient for testing application file operations, but **it doesn't verify that the package's custom creator is registered and executed correctly**.

After loading `ArchiveServiceProvider` in your [package test environment](/en/advanced/package-testing), verify the following separately.

| Test | What to verify |
| - | - |
| Normal boot | If no disk is used, configuration validation and file operations inside the creator don't start |
| Named disks | `reports` and `exports` each receive their own configuration and become different instances |
| Retrieving the same name again | Calling `disk('reports')` twice returns the same instance |
| Configuration change | Changing the configuration alone keeps the same instance; after `forgetDisk()` you get a different instance |
| On-demand | Calling `build()` twice with the same configuration returns different instances |
| Invalid configuration | In this example, resolving a disk without `root` throws an exception |
| Actual reads and writes | Verify storing, retrieving, and deleting in a test-only directory, and clean up afterward |
| With configuration cache | The driver is registered and resolvable in a freshly booted verification app |

For a custom external backend, separate creation tests that don't use the network from connection tests in an explicitly prepared verification environment. Don't treat a passing fake as evidence of compatibility with the real service.

## Long-term maintenance checklist

* Document the driver name, configuration keys, return value API, and the handling of exceptions and `throw` as a public specification.
* Use an extension name that doesn't collide with built-in drivers or other packages.
* Don't resolve or use disks just by booting the provider.
* When configuration changes, distinguish between the configuration cache, the Manager's retention array, references held by calling code, and other processes.
* Continuously verify creation, re-resolution, and actual file operations for each supported Laravel version.

## Primary sources consulted

* [Laravel official documentation: Custom filesystems](https://github.com/laravel/docs/blob/12080c72699b/filesystem.md#custom-filesystems)
* [FilesystemManager: disk, build, resolve, forgetDisk, purge, extend](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Filesystem/FilesystemManager.php)
* [FilesystemServiceProvider: singleton registration of the Manager](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Filesystem/FilesystemServiceProvider.php)
* [Storage facade: replacement with a local disk via fake](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Support/Facades/Storage.php)
* [Laravel official tests: creating and removing disks, custom closures](https://github.com/laravel/framework/blob/v13.37.0/tests/Filesystem/FilesystemManagerTest.php)


## Related topics

- [Registering package Artisan commands and resolving dependencies](/en/advanced/package-commands.md)
- [Package service resolution hooks](/en/advanced/package-service-resolution.md)
- [Package service lifetime design](/en/advanced/package-service-lifetimes.md)
- [Registering and caching package event listeners](/en/advanced/package-events.md)
- [Laravel Octane](/en/octane.md)


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