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

# 包的 Storage 驱动注册与重新解析

> 解读 Laravel 13 的 FilesystemManager，讲解 Storage::extend() 的注册、磁盘实例的保留、配置变更后的重新解析，以及不依赖 fake 的验证。

要从包中提供存储功能，需要把**注册驱动的创建方式**与**创建使用者的磁盘**区分开来。如果设计成仅靠服务提供者启动不会开始外部连接或文件操作，那么不使用该功能的环境在启动时也不容易被破坏。

本页基于对 Laravel Framework `v13.37.0` 实现的确认。基本操作请参阅[文件存储](/zh-CN/filesystem)，服务提供者的加载请参阅[包自动发现](/zh-CN/advanced/package-discovery)。

## 区分驱动名与磁盘名

`Storage::extend()` 的第 1 个参数是**驱动名**。而 `Storage::disk()` 的参数是在 `filesystems.disks` 中定义的**磁盘名**。可以定义多个使用同一驱动的磁盘。

| 要素 | 本页示例 | 作用 |
| - | - | - |
| 驱动名 | `acme-archive-local` | 创建磁盘的回调的标识符 |
| 磁盘名 | `reports`、`exports` | 配置与已创建实例的标识符 |
| 磁盘配置 | `root`、`throw` 等 | 创建该磁盘时传给回调的值 |

`FilesystemManager::resolve()` 会查找与配置中 `driver` 对应的自定义创建处理，如果存在，则优先于内置驱动调用它。如果把 `local` 或 `s3` 用作扩展名，就会替换已有的创建处理，因此请选择包专属的名称。

## 在服务提供者中只注册创建方式

下面是为了理解注册路径而复用 Laravel 标准本地驱动创建处理的最小示例。如果只是为了本地存储，使用普通的 `local` 就足够了。如果要提供自定义后端，请像官方文档的示例那样构建 Flysystem 适配器，并替换为用 Laravel 的 `FilesystemAdapter` 包装的处理。

```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);
        });
    }
}
```

该服务提供者按照[常规方式注册到包中](/zh-CN/advanced/package-discovery)。为了能在 HTTP、Artisan 和队列 worker 中使用，不要用 `runningInConsole()` 限制注册。另外，只负责注册扩展的服务提供者不要设为[延迟服务提供者](/zh-CN/advanced/deferred-provider)。

回调会接收应用实例以及**待解析磁盘的完整配置**。配置验证和客户端创建应在回调内进行，`boot()` 中不要执行文件操作。示例中的 `createLocalDriver()` 在创建时可能会创建目录，因此区分注册时与创建时非常重要。

<Tip>
  `FilesystemManager::extend()` 会将普通闭包的 `$this` 绑定到 Manager。不要假定可以使用服务提供者的 `$this`，像本例这样使用 `static` 闭包和参数 `$app`，可以让依赖关系更加清晰。
</Tip>

使用者在 `config/filesystems.php` 的 `disks` 数组中添加配置。

```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,
],
```

使用方指定的是磁盘名，而不是驱动名。

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

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

<Info>
  本例只展示本地存储的创建路径。通过 `serve` 提供文件或提供临时 URL 不在讨论范围内。即使在自定义适配器中支持 URL 生成、可见性、只读等功能，也请分别确认各自的实现与所需的依赖包，不要认为只要传入配置键就会生效。
</Info>

## 命名磁盘在创建后会被保留

`disk('reports')` 并不会每次都调用回调。Manager 的 `get()` 会优先使用内部 `$disks` 数组中已创建的实例。只有在尚未创建时才会经过 `resolve()`，创建结果以磁盘名为键保留下来。

```mermaid theme={null}
flowchart TD
    A["服务提供者的 boot()"] --> B["通过 extend() 注册创建处理"]
    B --> C["Storage::disk('reports')"]
    C --> D{"reports 已创建?"}
    D -->|是| E["返回保留的实例"]
    D -->|否| F["读取 driver，将配置传给创建处理"]
    F --> G["将创建结果保留为 reports"]
    G --> E
```

因此，即使在同一进程中执行以下操作，已创建的磁盘也不会自动更新。

* 通过 `config()` 修改 `filesystems.disks.reports`。
* 对同一驱动名再次调用 `Storage::extend()`。

`FilesystemServiceProvider` 也会将 Manager 本身注册为 singleton。在长时间运行的进程中，不要假定每个请求或任务都一定会重新创建磁盘。

## 配置变更后只重新解析需要的磁盘

在同一进程中修改配置并进行验证时，要把配置更新与移除保留的磁盘分开处理。

```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()` 会从 Manager 的保留数组中移除指定名称。下一次调用 `disk()` 时会重新读取当前配置。在所确认的版本中，`purge()` 的实现同样是移除指定名称，并不会调用自定义后端的连接关闭方法。

<Warning>
  像 `$before` 这样由使用方持有的引用并不会被替换。singleton 服务持有旧磁盘的情况也是如此。不要把 `forgetDisk()` 当作批量更新所有引用或其他进程的机制。
</Warning>

在生产环境修改配置或更新包时，请按照常规部署流程重新生成配置缓存，并重启长时间运行的 worker。配置缓存与本进程内的磁盘保留数组是两回事。

### 临时配置使用 build

如果不想修改命名的共享配置，可以使用[按需磁盘](/zh-CN/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()` 会使用指定配置直接调用 `resolve()`，不会将结果加入命名磁盘的保留数组。虽然需要注册自定义驱动，但每次调用都会进入创建处理。即使使用动态存储位置，也不要设计成可以根据用户输入无限制地选择存储位置或连接目标。

## 在 fake 之外单独测试创建路径

`Storage::fake('reports')` 会通过 Manager 的 `set()` 将该磁盘替换为测试用的本地磁盘。它对应用的文件操作测试很方便，但**并不能验证包的自定义创建处理是否被正确注册和执行**。

在[包的测试环境](/zh-CN/advanced/package-testing)中加载 `ArchiveServiceProvider` 后，分别验证以下内容。

| 测试 | 确认内容 |
| - | - |
| 常规启动 | 未使用磁盘时，创建处理内的配置验证和文件操作不会开始 |
| 命名磁盘 | `reports` 与 `exports` 分别接收各自的配置，并成为不同的实例 |
| 同名再次获取 | 调用两次 `disk('reports')` 得到同一个实例 |
| 配置变更 | 仅修改配置时仍是同一实例，`forgetDisk()` 之后成为不同实例 |
| 按需 | 使用相同配置调用两次 `build()` 也会得到不同实例 |
| 无效配置 | 在本例中，解析缺少 `root` 的磁盘时会抛出异常 |
| 实际读写 | 在测试专用目录中验证保存、读取、删除，并在结束时清理 |
| 存在配置缓存 | 在新启动的验证应用中驱动同样已注册并且可以解析 |

对于自定义外部后端，要把不使用网络的创建测试与在明确准备的验证环境中进行的连接测试分开。不要把 fake 的成功当作与真实服务兼容的证据。

## 长期维护检查清单

* 将驱动名、配置键、返回值的 API、异常及 `throw` 的处理作为公开规范写入文档。
* 使用不会与标准驱动或其他包冲突的扩展名。
* 不要仅在服务提供者启动时就解析或使用磁盘。
* 修改配置时，区分配置缓存、Manager 的保留数组、使用方的引用以及其他进程。
* 针对 Laravel 的每个支持版本，持续验证创建、重新解析以及实际的文件操作。

## 参考的一手资料

* [Laravel 官方文档: 自定义文件系统](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: Manager 的 singleton 注册](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Filesystem/FilesystemServiceProvider.php)
* [Storage 门面: 通过 fake 替换为本地磁盘](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Support/Facades/Storage.php)
* [Laravel 官方测试: 磁盘的创建·移除·自定义闭包](https://github.com/laravel/framework/blob/v13.37.0/tests/Filesystem/FilesystemManagerTest.php)


## Related topics

- [包的 Artisan 命令注册与依赖解析](/zh-CN/advanced/package-commands.md)
- [包的服务解析后钩子](/zh-CN/advanced/package-service-resolution.md)
- [包的路由注册与缓存](/zh-CN/advanced/package-routes.md)
- [包的事件监听器注册与缓存](/zh-CN/advanced/package-events.md)
- [包的中间件注册与适用范围](/zh-CN/advanced/package-middleware.md)


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