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

# 包的数据库连接扩展

> 解读 Laravel 13 的 DB::extend 与 Connection::resolverFor，讲解连接名称与驱动名称的优先顺序、连接的保留、重新创建以及包的测试。

要从包中扩展数据库连接，应将**创建方式的注册**与**所用连接的选择**分离开来。设计时要确保仅安装包不会替换应用的默认连接或所有 MySQL 连接。

本页面基于对 Laravel Framework `v13.40.0` 官方源码的确认。连接配置的基础请参阅[数据库配置](https://kawax.biz/zh-CN/database.md)，服务提供者的加载请参阅[包的自动发现](https://kawax.biz/zh-CN/advanced/package-discovery.md)。

## 选择扩展点

数据库的创建处理中，存在适用范围不同的扩展点。

| 扩展点 | 适用范围 | 回调的参数 |
| - | - | - |
| `DB::extend('连接名称', $resolver)` | 指定名称的连接 | 配置数组、连接名称 |
| `DB::extend('驱动名称', $resolver)` | 使用该驱动的连接。但连接名称的扩展优先 | 配置数组、连接名称 |
| `Connection::resolverFor('sqlite', $resolver)` | 由标准 ConnectionFactory 创建的该驱动的 Connection | PDO 或用于创建的 Closure、数据库名、表前缀、配置数组 |

传给 `DB::extend()` 的回调的返回值不是 PDO 或 Query Builder，而是 `Illuminate\Database\Connection`。`DatabaseManager::configure()` 要求这个类。

> [!WARNING]
>
> 数据库的 `extend()` 与通用的 `Illuminate\Support\Manager` 调用约定不同。回调的第一个参数不是应用容器，而是**连接配置数组**。请不要直接照搬其他驱动扩展的示例。

## 连接名称的扩展优先于驱动名称

`DatabaseManager::makeConnection()` 在获取配置后，按以下顺序选择创建方式。

```mermaid theme={null}
flowchart TD
    A["通过 DB::connection 指定连接名称"] --> B{"是否有已创建的连接？"}
    B -->|有| C["返回已保留的 Connection"]
    B -->|无| D["获取连接配置并解析 URL 配置"]
    D --> E{"是否有连接名称的扩展？"}
    E -->|有| F["执行连接名称的 resolver"]
    E -->|无| G{"是否有 driver 的扩展？"}
    G -->|有| H["执行 driver 的 resolver"]
    G -->|无| I["通过标准 ConnectionFactory 创建"]
    F --> J["设置事件、重新连接处理等并保留"]
    H --> J
    I --> J
```

* `DB::extend('reporting', ...)` 适用于 `database.connections.reporting`。
* `DB::extend('mysql', ...)` 适用于连接名称一侧没有扩展、且 `driver` 为 `mysql` 的连接。
* 两者使用同一个扩展注册表。考虑到连接名称与驱动名称的冲突，应使用包专属的名称。
* `extend()` 不会创建配置数组。仅注册扩展而不存在指定名称的连接配置时，会抛出异常。

## 显式选择自定义驱动名称

下面是将自定义名称 `acme-sqlite` 桥接到标准 SQLite 连接创建的最小示例。**它并非实现新的数据库协议或 SQL 方言的示例**。它展示的是分发自定义适配器时，注册与选择之间的边界。

假设 `Acme\DatabaseBridge\` 通过 Composer 的 PSR-4 映射到 `src/`，放置在 `src/DatabaseBridgeServiceProvider.php` 中。

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

namespace Acme\DatabaseBridge;

use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

final class DatabaseBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $app = $this->app;

        DB::extend('acme-sqlite', function (array $config, string $name) use ($app): Connection {
            $config['driver'] = 'sqlite';

            return $app->make('db.factory')->make($config, $name);
        });
    }
}
```

`db.factory` 是 Laravel 共享的 `ConnectionFactory` 绑定。由于直接调用标准 Factory，不会回到 DatabaseManager 的扩展选择，从而可以复用 SQLite 的延迟 PDO 创建与 Connection 创建。

此回调在首次解析连接时执行。服务提供者的 `boot()` 只负责注册创建方式，不执行连接测试或迁移。应作为常规的非延迟服务提供者，在目标连接首次被解析之前加载。

使用方应用在 `config/database.php` 现有的 `connections` 数组中添加连接。不要修改默认的 `default`。

```php theme={null}
'acme_reports' => [
    'driver' => 'acme-sqlite',
    'database' => database_path('reports.sqlite'),
    'prefix' => '',
    'foreign_key_constraints' => true,
],
```

请事先准备好 SQLite 文件，并确认写入权限后再使用。不要设计成仅加载包就创建文件。

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

$connection = DB::connection('acme_reports');
$rows = $connection->select('select 1 as value');
```

使用者传入的是连接名称 `acme_reports`，而不是驱动名称 `acme-sqlite`。示例中在传给 Factory 之前将 `driver` 转换为 `sqlite`，因此创建后的 `getDriverName()` 也是 `sqlite`。

> [!TIP]
>
> 如果在 resolver 内部再次调用 `DB::connection($name)`，会回到尚未保留的同一连接的创建处理。为避免递归，请通过 Factory 或自定义适配器创建 Connection。

## 替换为 Connection 子类属于另一个范围

如果要替换现有驱动的 Connection 类本身，可以使用 `Connection::resolverFor()`。传给回调的**有时并非立即连接的 PDO，而是延迟创建 PDO 的 Closure**。

不过，此注册保存在 Connection 类的 static 属性中。如果为 `sqlite` 注册，会影响通过标准 Factory 新创建的所有 SQLite 连接，上面的 `acme-sqlite` 示例也在其中。它不能被视为只针对某一个连接名称的替换。

* 回调中应返回目标数据库专用的 Connection 子类。仅使用基础 Connection 无法保留数据库专有的 Grammar 和 Processor 等。
* Factory 将 `createConnector()` 与 `createConnection()` 分开。仅注册 `resolverFor()` 并不会为未知驱动提供 PDO 连接器。
* 如果 `DB::extend()` 的自定义 resolver 不使用标准 Factory，也不会经过 Factory 内对 `resolverFor()` 的检查。
* 对同一驱动重复注册会覆盖原有注册。请确认与其他包的冲突，以及在同一 PHP 进程内的测试之间的残留。

希望限制影响范围的包，应首先考虑使用专属连接名称或驱动名称的 `DB::extend()`。

## 区分 reconnect 与 purge

DatabaseManager 按连接名称保留已创建的 Connection。即使修改配置或 resolver，也不会自动反映到已经创建的连接。

| 操作 | 对已保留 Connection 的影响 |
| - | - |
| `DB::extend()` | 注册或覆盖下一次的创建处理。现有对象保持不变 |
| `DB::disconnect($name)` | 断开 PDO，但保留 Connection |
| `DB::reconnect($name)` | 若已保留，则从新创建的连接中迁移 PDO 等。返回原来的 Connection |
| `DB::purge($name)` | 断开连接并从 Manager 的保留对象中移除。下次调用 `connection()` 时重新创建 |
| `DB::forgetExtension($key)` | 删除 resolver 的注册。不删除配置或已创建的连接 |

特别是 `reconnect()`，它并不是整体替换已保留 Connection 的类或配置数组的 API。如果要反映扩展类或连接配置的变更，请在没有进行中事务的安全时机执行 `purge()`，然后获取新的连接。

> [!WARNING]
>
> `purge()` 不会将其他服务或变量持有的 Connection 引用替换为新的引用。此外，连接名称的 `::read`、`::write`、`::direct` 是不同的保留键，不要以为仅 purge 基础名称就会将它们一并删除。
>
> 不要为了切换租户等目的而轻易改写运行中的共享连接配置。对于长生命周期的 worker，除了更新配置缓存外，还要确认进程的重启以及对已保留服务的影响。

## 测试包的注册与创建

在 [Orchestra Testbench](https://kawax.biz/zh-CN/advanced/package-testing.md) 中加载服务提供者，并实际选择自定义驱动。下面是 `tests/DatabaseBridgeTest.php` 的示例。运行环境需要 PDO 和 SQLite 驱动。

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

namespace Acme\DatabaseBridge\Tests;

use Acme\DatabaseBridge\DatabaseBridgeServiceProvider;
use Illuminate\Database\SQLiteConnection;
use Orchestra\Testbench\TestCase;

final class DatabaseBridgeTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [DatabaseBridgeServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.acme_reports', [
            'driver' => 'acme-sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    public function test_registration_does_not_create_connections(): void
    {
        $manager = $this->app->make('db');

        $this->assertSame('testing', $manager->getDefaultConnection());
        $this->assertArrayNotHasKey('acme_reports', $manager->getConnections());
    }

    public function test_connection_is_reused_and_purge_creates_a_new_one(): void
    {
        $manager = $this->app->make('db');
        $first = $manager->connection('acme_reports');

        $this->assertInstanceOf(SQLiteConnection::class, $first);
        $this->assertSame('acme_reports', $first->getName());
        $this->assertSame('sqlite', $first->getDriverName());
        $this->assertSame(1, (int) $first->selectOne('select 1 as value')->value);
        $this->assertSame($first, $manager->connection('acme_reports'));
        $this->assertSame($first, $manager->reconnect('acme_reports'));

        $manager->purge('acme_reports');

        $second = $manager->connection('acme_reports');

        $this->assertNotSame($first, $second);
        $this->assertSame(1, (int) $second->selectOne('select 1 as value')->value);
    }
}
```

`:memory:` 在连接断开时会丢失内容，因此此测试不期望保留表或数据。在实际的包中，还应添加以下测试。

* 连接名称的 resolver 优先于驱动名称的 resolver。
* 同名重复注册和 `forgetExtension()` 不会替换已创建的 Connection。
* 缺少必需配置时的异常，以及连接失败时的行为。
* 在支持的数据库上的迁移、事务、读写分离等承诺提供的功能。
* 在长生命周期 worker 或与多个包并用时，注册和连接状态不会混杂。

## 维护时的检查项目

* 是否通过自定义名称选择启用，而没有悄悄覆盖默认连接或内置驱动。
* 是否没有将 Closure 或连接对象放入配置缓存，而是将创建处理放在服务提供者中。
* 是否将连接目标和认证信息作为可信的应用配置进行管理。
* 是否在受支持的 Laravel 版本中重新确认了 `extend()` 的参数、Factory 的创建顺序以及 `reconnect()` 的迁移对象。
* 是否向使用者说明了配置变更后的重新创建步骤与 worker 重启。

## 相关页面

* [包开发](https://kawax.biz/zh-CN/advanced/package-development.md)
* [包服务的生命周期设计](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)
* [包配置的合并与缓存](https://kawax.biz/zh-CN/advanced/package-config-merging.md)
* [包迁移的发布与更新](https://kawax.biz/zh-CN/advanced/package-migrations.md)

## 一手资料

* [Laravel 官方：数据库与多个连接](https://github.com/laravel/docs/blob/13.x/database.md#using-multiple-database-connections)
* [Laravel 官方：包的服务提供者](https://github.com/laravel/docs/blob/13.x/packages.md#service-providers)
* [DatabaseManager：创建顺序、扩展注册、连接的保留与重新连接](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/DatabaseManager.php)
* [ConnectionFactory：PDO 的延迟创建与 Connection resolver](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Connectors/ConnectionFactory.php)
* [Connection：resolverFor 与 getResolver](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Connection.php)
* [DatabaseServiceProvider：db 与 db.factory 的注册](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/DatabaseServiceProvider.php)


## Related topics

- [数据库配置](https://kawax.biz/zh-CN/database.md)
- [Pipeline 模式](https://kawax.biz/zh-CN/advanced/pipeline.md)
- [Laravel Boost](https://kawax.biz/zh-CN/boost.md)
- [部署](https://kawax.biz/zh-CN/deployment.md)
- [并发处理](https://kawax.biz/zh-CN/concurrency.md)


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