> ## 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-TW/database.md)，提供者的載入請參閱[套件自動探索](https://kawax.biz/zh-TW/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` 呼叫慣例不同。回呼的第 1 個參數不是應用程式容器，而是**連線設定的陣列**。請不要直接沿用其他驅動擴充的範例。

## 連線名稱的擴充優先於驅動名稱

`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-TW/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 或連線物件放入 config 快取，並將建立處理放在提供者中。
* 是否將連線目標與認證資訊作為可信任的應用程式設定加以管理。
* 是否已在支援的 Laravel 版本中重新確認 `extend()` 的參數、Factory 的建立順序以及 `reconnect()` 的移轉對象。
* 是否已向使用者說明設定變更後的重新建立步驟與 worker 重新啟動。

## 相關頁面

* [套件開發](https://kawax.biz/zh-TW/advanced/package-development.md)
* [套件服務的生命週期設計](https://kawax.biz/zh-TW/advanced/package-service-lifetimes.md)
* [套件設定的合併與快取](https://kawax.biz/zh-TW/advanced/package-config-merging.md)
* [套件遷移的發布與更新](https://kawax.biz/zh-TW/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-TW/database.md)
- [並行處理](https://kawax.biz/zh-TW/concurrency.md)
- [Pipeline 模式](https://kawax.biz/zh-TW/advanced/pipeline.md)
- [Laravel Boost](https://kawax.biz/zh-TW/boost.md)
- [部署](https://kawax.biz/zh-TW/deployment.md)


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