> ## 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 的 CacheManager 與 Store 契約，驗證驅動的註冊時機、Repository 的 TTL 轉換、實例保留，以及鎖等功能的支援範圍。

想從套件提供自訂的快取後端時，請將**實作儲存處理的 Store** 與**提供 Laravel API 的 Repository** 分開。註冊到 `Cache::extend()` 的是建立方式，Laravel 並不會自動將回傳的 Store 包裝成 Repository。

本頁確認了 Laravel Framework `v13.39.0` 的實作。基本操作請參閱[快取](https://kawax.biz/zh-TW/cache.md)，提供者的準備請參閱[套件開發](https://kawax.biz/zh-TW/advanced/package-development.md)。

## 區分驅動名稱與儲存名稱

| 要素 | 本頁的範例 | 角色 |
| - | - | - |
| 驅動名稱 | `acme-memory` | 以 `extend()` 註冊的建立方式 |
| 儲存名稱 | `catalog` | 定義於 `cache.stores` 的設定，以及已建立 Repository 的識別名稱 |
| Store | `Illuminate\Cache\ArrayStore` | 鍵的儲存、讀取與有效期限管理 |
| Repository | `Illuminate\Cache\Repository` | `remember()`、TTL 轉換、快取事件等 |

若設定的 `driver` 有對應的自訂建立處理，`CacheManager::build()` 會比內建驅動更優先呼叫它。請不要重複使用 `redis` 或 `array` 等名稱，而是使用套件專屬的名稱。

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"catalog 已解析？"}
    B -->|是| C["回傳已保留的 Repository"]
    B -->|否| D["讀取 cache.stores.catalog"]
    D --> E["driver: acme-memory 的建立處理"]
    E --> F["建立 Store 並<br>以 repository() 包裝"]
    F --> G["以 catalog 名稱保留"]
```

## 在其他提供者的 boot 之前註冊

若其他套件會在 `boot()` 中使用快取，而你在自己的 `boot()` 中註冊驅動，依順序不同可能會來不及。請依照官方文件，在 `register()` 中註冊**應用程式的 `booting` 回呼**。

以下是為了在不依賴外部服務的情況下確認註冊路徑，而使用標準 `ArrayStore` 的最小範例。請放在 `Acme\Memo` 套件的 `src/MemoServiceProvider.php`。

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

namespace Acme\Memo;

use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;

class MemoServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->booting(static function (Application $app): void {
            $app['cache']->extend('acme-memory', static function (
                Application $app,
                array $config,
            ): Repository {
                $store = new ArrayStore(
                    $config['serialize'] ?? false,
                    $app['config']->get('cache.serializable_classes'),
                );

                return $app['cache']->repository($store, $config);
            });
        });
    }
}
```

提供者可透過[套件自動探索](https://kawax.biz/zh-TW/advanced/package-discovery.md)等方式載入。這個只負責註冊擴充的提供者不要設為延遲提供者，讓它在 HTTP 與 Artisan 兩種情境下都會被載入。

* 在一般啟動流程中，應用程式的 `booting` 會在所有提供者的 `register()` 完成之後、各個 `boot()` 之前執行。這與提供者本身的 `$this->booting()` 不同。
* `extend()` 的回呼會收到應用程式與儲存設定陣列。Store 的建立與外部連線請放在這個建立用回呼中。
* 這個註冊時機並不保證**在其他提供者的 `register()` 中使用快取**也能運作。請將服務的註冊與實際使用分開。

> [!TIP]
>
> 若只需要標準的記憶體內快取，使用 `array` 驅動就足夠了。這個範例是用來說明註冊與契約，無法用於跨行程的共享或持久化。實作自訂後端時，請將 `ArrayStore` 的建立部分替換為實作 `Illuminate\Contracts\Cache\Store` 的類別。

`CacheManager::extend()` 會將匿名閉包的作用域重新綁定到 Manager。請不要以為非 static 閉包中的 `$this` 是提供者，而是像範例一樣使用 static 閉包與引數 `$app`。

### 在使用端應用程式中新增儲存

在 `config/cache.php` 既有的 `stores` 陣列中加入下列內容。請不要將套件設計成會改寫使用端的 `default` 或整個 `stores`。

```php theme={null}
'catalog' => [
    'driver' => 'acme-memory',
    'serialize' => true,
],
```

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

$value = Cache::store('catalog')->remember('product:42', 60, function () {
    return ['id' => 42, 'name' => 'Sample'];
});
```

若想設為預設值，在 `CACHE_STORE` 中指定的也是\*\*`catalog` 這個儲存名稱\*\*，而不是驅動名稱。為同一個驅動建立多組設定時，每個儲存名稱都會建立各自的 Repository。

## 自行組裝 Repository

`CacheManager::callCustomCreator()` 會直接回傳回呼的回傳值。若只回傳 Store，即使 `get()` 與 `put()` 能運作，也無法使用 Repository 的 `remember()` 等方法，TTL 的解讀方式也會改變。

範例中的 `repository($store, $config)` 負責下列事項。

* 以 `Illuminate\Cache\Repository` 包裝 Store。
* 將 Manager 加入設定的 `store` 名稱傳給 Repository。
* 若已註冊事件 Dispatcher，且 `events` 設定未停用，就將 Dispatcher 設定給 Repository。

這並不是將所有自訂設定套用到 Store 的處理。連線目標、prefix、序列化等，需由 Store 的實作與建立處理自行反映。範例中讀取的 `serialize` 設定也是如此。

## 對照目前的實作確認 Store 契約

請實作 Laravel 13 的 `Illuminate\Contracts\Cache\Store` 所要求的方法。不要將過去範例中的空方法清單直接當成可運作的驅動發布。

| 方法 | 要確認的行為 |
| - | - |
| `get()` / `many()` | 未命中或已過期時回傳 `null`。`many()` 回傳以要求的鍵為鍵的陣列，未命中的部分也設為 `null` |
| `put()` / `putMany()` | 以秒為單位的 TTL 儲存，並回傳成功或失敗 |
| `forever()` | 不設定有效期限地儲存。並不代表能防止後端的逐出或刪除 |
| `touch()` | 變更既有鍵的有效期限。未命中或已過期時回傳 `false` |
| `increment()` / `decrement()` | 回傳更新後的值或失敗。也要驗證並行更新與 TTL 的維持 |
| `forget()` | 刪除鍵並回傳結果 |
| `flush()` | 刪除整個快取。請明確說明實際被刪除的範圍 |
| `getPrefix()` | 回傳 Store 使用的鍵前綴 |

`null` 會被 Repository 視為快取未命中。實作時請不要將 `false` 或 `0` 也當成未命中。即使儲存 `null`，也無法作為防止 `remember()` 重新計算的標記。

### 透過 Repository 驗證 TTL

`Repository::put()` 會在傳給 Store 之前處理 TTL。

| 對 Repository 的呼叫 | 對 Store 的處理 |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | 傳入秒數的 `put()` |
| `put('key', $value, $expiresAt)` | 從日期時間等轉換為秒數，若為正值則呼叫 `put()` |
| `put('key', $value, 0)` 或負的 TTL | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` 或負的 TTL | `forget()` |

> [!WARNING]
>
> 範例中的 `ArrayStore` 若直接對 `put()` 傳入 `0` 秒，會當成永久儲存。另一方面，Repository 的 `put(..., 0)` 則是刪除。只測試以 `getStore()` 取出的低階 Store，無法確認使用者實際呼叫的 API 行為。

### add、鎖與標籤需另外確認支援情況

僅實作 Store 契約，並不保證支援所有快取功能。

* **原子性的 `add()`**：不包含在 Store 契約中。以正的 TTL 呼叫時，若 Store 有 `add()`，Repository 就會使用它。若沒有，則會退回以 `get()` 與 `put()` 實現，因此僅以 Repository 包裝無法保證原子性。在共享後端中，請以該後端的原子操作實作，並驗證並行執行。
* **鎖**：`Illuminate\Contracts\Cache\LockProvider` 的 `lock()` 與 `restoreLock()` 是另一個契約。請驗證取得、擁有者一致、過期，以及不會釋放其他擁有者的鎖。範例中 `ArrayStore` 的鎖無法提供跨行程的互斥控制。
* **標籤**：Repository 會確認 Store 是否有 `tags()`。一般需要使用 `TaggableStore` 等進行實作，僅靠 Store 契約不會自動啟用。

請不要認為 `flush()` 是會尊重設定中 prefix 的部分刪除。請確認在共享後端中的刪除範圍，並連同 `php artisan cache:clear` 一起向使用者說明。

## 區分實例的保留與資料的刪除

`CacheManager` 是 singleton，`store('catalog')` 的結果也會依儲存名稱保留。僅變更設定或重新註冊 `extend()`，並不會更新已建立的 Repository。

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

$before = Cache::store('catalog');

config(['cache.stores.catalog.serialize' => false]);

$same = Cache::store('catalog');

Cache::forgetDriver('catalog');

$after = Cache::store('catalog');
```

`$same` 與 `$before` 是同一個物件。在 `forgetDriver()` 之後取得時，才會依新的設定建立 `$after`。

| 操作 | 對象 |
| - | - |
| `Cache::forgetDriver('catalog')` | 移除 Manager 保留的 Repository 參照 |
| `Cache::store('catalog')->forget('key')` | 刪除已儲存的單一鍵 |
| `Cache::store('catalog')->flush()` | 刪除 Store 的所有快取資料 |

`forgetDriver()` 並不會刪除後端的所有資料。不過，範例中的 `ArrayStore` 將資料保存在實例內，因此新的 Store 看不到舊資料。此外，使用端的 `$before`，以及已解析的 `cache.store` singleton 所保留的舊 Repository 參照，都不會被替換。

在正式環境中，請依照一般的部署流程更新設定快取，並重新啟動長時間執行的 worker。也請考量[服務的生命週期](https://kawax.biz/zh-TW/advanced/package-service-lifetimes.md)，不要將重新解析視為所有行程的一次性更新。

## 不只模擬 Facade，也要測試建立路徑

在[套件的測試環境](https://kawax.biz/zh-TW/advanced/package-testing.md)中，讓處理實際經過 Manager 與 Repository。下面的 Testbench 範例會確認，在先註冊的另一個提供者的 `boot()` 中也能使用該驅動。範例在不變更 `cache.serializable_classes` 還原限制的情況下儲存陣列，並以 `ArrayStore::all(false)` 確認原始的儲存值。

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

namespace Acme\Memo\Tests;

use Acme\Memo\MemoServiceProvider;
use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Orchestra\Testbench\TestCase;

class CacheConsumerProvider extends ServiceProvider
{
    public function boot(): void
    {
        Cache::store('catalog')->forever('boot-marker', 'ready');
    }
}

class PackageCacheTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [CacheConsumerProvider::class, MemoServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('cache.default', 'array');
        $app['config']->set('cache.stores.catalog', [
            'driver' => 'acme-memory',
            'serialize' => true,
        ]);
    }

    public function test_the_driver_is_available_during_another_providers_boot(): void
    {
        $cache = Cache::store('catalog');

        $this->assertInstanceOf(Repository::class, $cache);
        $this->assertInstanceOf(ArrayStore::class, $cache->getStore());
        $this->assertSame('catalog', $cache->getName());
        $this->assertSame('ready', $cache->get('boot-marker'));
        $this->assertNotNull($cache->getEventDispatcher());
    }

    public function test_serialization_is_applied_from_the_store_configuration(): void
    {
        $cache = Cache::store('catalog');
        $value = ['status' => 'queued'];

        $cache->put('job', $value, 60);

        $this->assertSame($value, $cache->get('job'));
        $this->assertSame(serialize($value), $cache->getStore()->all(false)['job']['value']);
    }

    public function test_a_zero_ttl_deletes_the_item_through_the_repository(): void
    {
        $cache = Cache::store('catalog');
        $cache->forever('product:42', 'old');

        $this->assertTrue($cache->put('product:42', 'new', 0));
        $this->assertNull($cache->get('product:42'));
    }

    public function test_forgetting_a_driver_rebuilds_only_the_manager_reference(): void
    {
        $before = Cache::store('catalog');

        config(['cache.stores.catalog.serialize' => false]);
        $this->assertSame($before, Cache::store('catalog'));

        Cache::forgetDriver('catalog');
        $after = Cache::store('catalog');

        $this->assertNotSame($before, $after);
        $this->assertSame('ready', $before->get('boot-marker'));
        $this->assertNull($after->get('boot-marker'));

        $value = ['status' => 'queued'];
        $after->forever('job', $value);

        $this->assertSame($value, $after->get('job'));
        $this->assertSame($value, $after->getStore()->all(false)['job']['value']);
    }
}
```

實作自訂後端時，除此之外還要驗證過期、`many()` 的未命中鍵、`false` 與 `0` 的往返、`touch()`、連線故障、並行更新，以及 prefix 與刪除範圍。不要將單純的 `Cache::shouldReceive('get')` 成功，當成驅動註冊或與外部服務相容的證據。

## 長期維護的檢查清單

* 將驅動名稱、儲存設定、序列化格式、例外與失敗時的回傳值列為公開規格。
* 依支援的 Laravel 版本，確認 Store 契約的方法，以及標籤、鎖等選用功能。
* 驗證在有設定快取的全新啟動下也能註冊，且能從其他提供者的 `boot()` 中使用。
* 變更儲存資料的相容性時，連同既有鍵的遷移或捨棄方針一併公告。
* 區分行程內的實例保留、後端的資料，以及設定快取。

## 參考的一手資料

* [Laravel 官方文件：自訂快取驅動](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/cache.md#adding-custom-cache-drivers)
* [CacheManager：build、repository、extend、forgetDriver](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Repository：TTL 轉換、add、tags、事件](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Store 契約](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [LockProvider 契約](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore：儲存、有效期限、序列化](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider：Manager 與 cache.store 的 singleton 註冊](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application：應用程式的 booting 與提供者的 boot](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Laravel 官方測試：CacheManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheManagerTest.php) / [CacheRepositoryTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheRepositoryTest.php)


## Related topics

- [進階主題](https://kawax.biz/zh-TW/advanced/index.md)
- [快取](https://kawax.biz/zh-TW/cache.md)
- [套件的 Blade 擴充與編譯快取](https://kawax.biz/zh-TW/advanced/package-blade-extensions.md)
- [套件的自訂通知頻道與傳送驗證](https://kawax.biz/zh-TW/advanced/package-notification-channels.md)
- [套件快取與 optimize 的整合](https://kawax.biz/zh-TW/advanced/package-optimization.md)


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