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

# Eigene Cache-Stores und Verträge in Paketen

> Analysiert den CacheManager und den Store-Vertrag von Laravel 13 und prüft den Registrierungszeitpunkt von Treibern, die TTL-Umrechnung durch das Repository, das Vorhalten von Instanzen sowie den Unterstützungsumfang etwa bei Locks.

Wenn ein Paket ein eigenes Cache-Backend bereitstellen soll, trennen Sie **den Store, der das Speichern implementiert**, vom **Repository, das die Laravel-API bereitstellt**. Mit `Cache::extend()` registrieren Sie eine Erzeugungsmethode; Laravel umhüllt den zurückgegebenen Store nicht automatisch mit einem Repository.

Diese Seite basiert auf der Implementierung von Laravel Framework `v13.39.0`. Die Grundlagen finden Sie unter [Cache](https://kawax.biz/de/cache.md), die Vorbereitung des Providers unter [Paketentwicklung](https://kawax.biz/de/advanced/package-development.md).

## Treibername und Store-Name trennen

| Element | Beispiel auf dieser Seite | Rolle |
| - | - | - |
| Treibername | `acme-memory` | Die mit `extend()` registrierte Erzeugungsmethode |
| Store-Name | `catalog` | Die in `cache.stores` definierte Konfiguration und der Bezeichner des erzeugten Repositorys |
| Store | `Illuminate\Cache\ArrayStore` | Speichern und Lesen von Schlüsseln, Verwaltung der Ablaufzeiten |
| Repository | `Illuminate\Cache\Repository` | `remember()`, TTL-Umrechnung, Cache-Events usw. |

`CacheManager::build()` ruft eine eigene Erzeugungsmethode für das konfigurierte `driver` auf, sofern vorhanden, und zwar noch vor den eingebauten Treibern. Verwenden Sie daher keine Namen wie `redis` oder `array` erneut, sondern einen paketspezifischen Namen.

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"catalog bereits aufgelöst?"}
    B -->|Ja| C["Vorgehaltenes Repository zurückgeben"]
    B -->|Nein| D["cache.stores.catalog laden"]
    D --> E["Erzeugungsmethode für driver: acme-memory"]
    E --> F["Store erzeugen und<br>mit repository() umhüllen"]
    F --> G["Unter dem Namen catalog vorhalten"]
```

## Vor dem boot anderer Provider registrieren

Wenn ein anderes Paket den Cache in seinem `boot()` verwendet, kann eine Registrierung des Treibers in Ihrem eigenen `boot()` je nach Reihenfolge zu spät kommen. Registrieren Sie gemäß der offiziellen Dokumentation in `register()` einen **`booting`-Callback der Anwendung**.

Das folgende Minimalbeispiel verwendet den Standard-`ArrayStore`, um den Registrierungsweg ohne externen Dienst zu prüfen. Es wird in `src/MemoServiceProvider.php` des Pakets `Acme\Memo` abgelegt.

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

Den Provider laden Sie etwa über die [automatische Paketerkennung](https://kawax.biz/de/advanced/package-discovery.md). Da dieser Provider nur die Erweiterung registriert, machen Sie ihn nicht zu einem verzögerten (deferred) Provider, damit er sowohl bei HTTP als auch bei Artisan geladen wird.

* Das `booting` der Anwendung wird beim normalen Start ausgeführt, nachdem `register()` aller Provider abgeschlossen ist und bevor die jeweiligen `boot()` laufen. Es ist nicht dasselbe wie `$this->booting()` des Providers selbst.
* Der Callback von `extend()` erhält die Anwendung und das Konfigurations-Array des Stores. Erzeugen Sie den Store und bauen Sie externe Verbindungen in diesem Erzeugungs-Callback auf.
* Dieser Registrierungszeitpunkt garantiert nicht die **Nutzung des Caches innerhalb von `register()`** anderer Provider. Trennen Sie die Registrierung von Diensten von ihrer tatsächlichen Nutzung.

> [!TIP]
>
> Wenn Sie lediglich einen Standard-In-Memory-Cache benötigen, genügt der Treiber `array`. Dieses Beispiel dient der Erläuterung von Registrierung und Verträgen und eignet sich weder für prozessübergreifendes Teilen noch für Persistenz. Bei einem eigenen Backend ersetzen Sie die Erzeugung des `ArrayStore` durch eine Klasse, die `Illuminate\Contracts\Cache\Store` implementiert.

`CacheManager::extend()` bindet den Scope anonymer Closures an den Manager neu. Gehen Sie nicht davon aus, dass `$this` in einer nicht-statischen Closure der Provider ist, sondern verwenden Sie wie im Beispiel eine statische Closure und das Argument `$app`.

### Den Store in der nutzenden Anwendung hinzufügen

Fügen Sie das folgende Element dem bestehenden `stores`-Array in `config/cache.php` hinzu. Gestalten Sie das Paket nicht so, dass es `default` oder das gesamte `stores` der nutzenden Anwendung überschreibt.

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

Auch wenn Sie den Store per `CACHE_STORE` als Standard festlegen möchten, geben Sie dort nicht den Treibernamen an, sondern **den Store-Namen `catalog`**. Legen Sie mehrere Konfigurationen mit demselben Treiber an, wird für jeden Store-Namen ein eigenes Repository erzeugt.

## Das Repository selbst zusammensetzen

`CacheManager::callCustomCreator()` gibt den Rückgabewert des Callbacks unverändert zurück. Geben Sie nur einen Store zurück, funktionieren zwar `get()` und `put()`, aber `remember()` und andere Methoden des Repositorys stehen nicht zur Verfügung, und auch die Interpretation der TTL ändert sich.

`repository($store, $config)` im Beispiel übernimmt Folgendes:

* Es umhüllt den Store mit `Illuminate\Cache\Repository`.
* Es übergibt den vom Manager zur Konfiguration hinzugefügten `store`-Namen an das Repository.
* Ist ein Event-Dispatcher registriert und die Einstellung `events` nicht deaktiviert, setzt es den Dispatcher im Repository.

Dabei werden nicht alle eigenen Einstellungen auf den Store angewendet. Verbindungsziel, Präfix, Serialisierung usw. setzen Sie in der Store-Implementierung und in der Erzeugungsmethode um. Das gilt auch für `serialize`, das im Beispiel aus der Konfiguration gelesen wird.

## Den Store-Vertrag mit der aktuellen Implementierung abgleichen

Implementieren Sie die Methoden, die `Illuminate\Contracts\Cache\Store` in Laravel 13 verlangt. Verteilen Sie keine Liste leerer Methoden aus älteren Beispielen als funktionierenden Treiber.

| Methode | Zu prüfendes Verhalten |
| - | - |
| `get()` / `many()` | Nicht vorhandene oder abgelaufene Einträge liefern `null`. `many()` gibt ein Array zurück, dessen Schlüssel die angefragten Schlüssel sind, wobei auch fehlende Einträge `null` sind |
| `put()` / `putMany()` | Speichert mit einer TTL in Sekunden und gibt Erfolg oder Misserfolg zurück |
| `forever()` | Speichert ohne Ablaufzeit. Das bedeutet nicht, dass eine Verdrängung oder Löschung durch das Backend verhindert wird |
| `touch()` | Ändert die Ablaufzeit eines vorhandenen Schlüssels. Gibt bei nicht vorhandenen oder abgelaufenen Einträgen `false` zurück |
| `increment()` / `decrement()` | Gibt den aktualisierten Wert oder einen Fehlschlag zurück. Prüfen Sie auch gleichzeitige Aktualisierungen und den Erhalt der TTL |
| `forget()` | Löscht den Schlüssel und gibt das Ergebnis zurück |
| `flush()` | Löscht den gesamten Cache. Machen Sie den tatsächlich gelöschten Umfang explizit |
| `getPrefix()` | Gibt das vom Store verwendete Schlüsselpräfix zurück |

`null` wird vom Repository als Cache-Miss behandelt. Implementieren Sie den Store so, dass `false` oder `0` nicht als fehlend gelten. Auch das Speichern von `null` dient nicht als Markierung, die eine Neuberechnung durch `remember()` verhindert.

### Die TTL über das Repository prüfen

`Repository::put()` verarbeitet die TTL, bevor sie an den Store übergeben wird.

| Aufruf am Repository | Verarbeitung im Store |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | `put()` mit Sekundenangabe |
| `put('key', $value, $expiresAt)` | Umrechnung von Datum/Uhrzeit usw. in Sekunden, bei positivem Wert `put()` |
| `put('key', $value, 0)` oder negative TTL | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` oder negative TTL | `forget()` |

> [!WARNING]
>
> Der `ArrayStore` im Beispiel speichert einen Eintrag ohne Ablaufzeit, wenn Sie direkt an `put()` `0` Sekunden übergeben. `put(..., 0)` am Repository bedeutet dagegen Löschen. Wenn Sie nur den mit `getStore()` geholten Low-Level-Store testen, prüfen Sie nicht das Verhalten der API, die Nutzer tatsächlich aufrufen.

### add, Locks und Tags gesondert prüfen

Allein die Implementierung des Store-Vertrags garantiert nicht die Unterstützung aller Cache-Funktionen.

* **Atomares `add()`**: Ist nicht Teil des Store-Vertrags. Bei einem Aufruf mit positiver TTL verwendet das Repository `add()` des Stores, sofern vorhanden. Andernfalls greift ein Fallback über `get()` und `put()`, sodass das bloße Umhüllen mit einem Repository keine Atomarität garantiert. Implementieren Sie es bei geteilten Backends mit den atomaren Operationen des jeweiligen Backends und prüfen Sie auch die gleichzeitige Ausführung.
* **Locks**: `lock()` und `restoreLock()` aus `Illuminate\Contracts\Cache\LockProvider` sind ein separater Vertrag. Prüfen Sie das Erwerben, die Übereinstimmung des Besitzers, den Ablauf und dass Locks anderer Besitzer nicht freigegeben werden. Die Locks des `ArrayStore` im Beispiel bieten keinen prozessübergreifenden gegenseitigen Ausschluss.
* **Tags**: Das Repository prüft, ob der Store `tags()` besitzt. In der Regel ist eine Implementierung etwa mit `TaggableStore` nötig; allein durch den Store-Vertrag werden Tags nicht automatisch aktiviert.

Gehen Sie nicht davon aus, dass `flush()` eine teilweise Löschung unter Berücksichtigung des konfigurierten Präfixes ist. Prüfen Sie den Löschumfang bei geteilten Backends und erläutern Sie ihn den Nutzern, einschließlich `php artisan cache:clear`.

## Vorhalten von Instanzen und Löschen von Daten unterscheiden

`CacheManager` ist ein Singleton und hält auch das Ergebnis von `store('catalog')` pro Store-Namen vor. Eine Konfigurationsänderung oder eine erneute Registrierung per `extend()` allein aktualisiert bereits erzeugte Repositorys nicht.

```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` ist dasselbe Objekt wie `$before`. Erst beim Abruf nach `forgetDriver()` wird `$after` aus der neuen Konfiguration erzeugt.

| Operation | Ziel |
| - | - |
| `Cache::forgetDriver('catalog')` | Entfernt die vom Manager vorgehaltene Referenz auf das Repository |
| `Cache::store('catalog')->forget('key')` | Löscht einen gespeicherten Schlüssel |
| `Cache::store('catalog')->flush()` | Löscht alle Cache-Daten des Stores |

`forgetDriver()` löscht nicht das gesamte Backend. Da der `ArrayStore` im Beispiel die Daten jedoch innerhalb der Instanz hält, sind alte Daten im neuen Store nicht sichtbar. Außerdem werden alte Repository-Referenzen nicht ersetzt, die von `$before` auf Nutzerseite oder vom bereits aufgelösten Singleton `cache.store` gehalten werden.

Aktualisieren Sie in Produktion den Konfigurations-Cache im Rahmen des regulären Deployments und starten Sie langlebige Worker neu. Berücksichtigen Sie auch die [Lebensdauer von Services](https://kawax.biz/de/advanced/package-service-lifetimes.md) und behandeln Sie eine erneute Auflösung nicht als gleichzeitige Aktualisierung aller Prozesse.

## Nicht nur Fassaden-Mocks, sondern den Erzeugungsweg testen

Leiten Sie in der [Testumgebung für Pakete](https://kawax.biz/de/advanced/package-testing.md) die Aufrufe durch den echten Manager und das echte Repository. Das folgende Testbench-Beispiel prüft, dass der Treiber auch im `boot()` eines zuvor registrierten anderen Providers verfügbar ist. Im Beispiel wird ein Array gespeichert, ohne die Wiederherstellungsbeschränkung von `cache.serializable_classes` zu ändern, und mit `ArrayStore::all(false)` wird auch der rohe gespeicherte Wert geprüft.

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

Bei einem eigenen Backend prüfen Sie zusätzlich Ablauf, fehlende Schlüssel bei `many()`, den Hin- und Rückweg von `false` und `0`, `touch()`, Verbindungsfehler, gleichzeitige Aktualisierungen sowie Präfix und Löschumfang. Ein bloß erfolgreiches `Cache::shouldReceive('get')` ist kein Beleg für die Treiberregistrierung oder die Kompatibilität mit einem externen Dienst.

## Checkliste für die langfristige Wartung

* Machen Sie Treibernamen, Store-Konfiguration, Serialisierungsformat sowie Exceptions und Rückgabewerte im Fehlerfall zur öffentlichen Spezifikation.
* Prüfen Sie die Methoden des Store-Vertrags und optionale Funktionen wie Tags und Locks für jede unterstützte Laravel-Version.
* Prüfen Sie, dass die Registrierung auch bei einem Neustart mit Konfigurations-Cache funktioniert und der Treiber aus dem `boot()` anderer Provider nutzbar ist.
* Kündigen Sie Kompatibilitätsänderungen an gespeicherten Daten zusammen mit einer Strategie für die Migration oder das Verwerfen bestehender Schlüssel an.
* Unterscheiden Sie zwischen dem Vorhalten von Instanzen im Prozess, den Daten im Backend und dem Konfigurations-Cache.

## Verwendete Primärquellen

* [Offizielle Laravel-Dokumentation: Eigene Cache-Treiber](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-Umrechnung, add, tags, Events](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Store-Vertrag](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [LockProvider-Vertrag](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore: Speichern, Ablaufzeiten, Serialisierung](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider: Singleton-Registrierung von Manager und cache.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application: booting der Anwendung und boot der Provider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Offizielle Laravel-Tests: 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

- [Cache](https://kawax.biz/de/cache.md)
- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Queues und Jobs](https://kawax.biz/de/queues.md)
- [Blade-Erweiterungen und Kompilierungscache in Paketen](https://kawax.biz/de/advanced/package-blade-extensions.md)
- [Eigene Benachrichtigungskanäle in Paketen und Prüfung der Zustellung](https://kawax.biz/de/advanced/package-notification-channels.md)


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