Skip to main content
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. For how providers are loaded, see 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. 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.
Register this provider in the package the usual way. 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. 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.
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.
Users add the configuration to the disks array in config/filesystems.php.
Calling code specifies the disk name, not the driver name.
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.

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. 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.
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.
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.
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.
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, verify the following separately. 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

Last modified on October 18, 2026