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

# Stores de cache personnalisés et contrats pour les packages

> Analysez le CacheManager et le contrat Store de Laravel 13 pour vérifier le moment d'enregistrement du driver, la conversion du TTL par le Repository, la conservation des instances et la prise en charge des verrous.

Pour fournir un backend de cache propre depuis un package, séparez **le Store qui implémente le stockage** du **Repository qui expose l'API de Laravel**. Ce que vous enregistrez avec `Cache::extend()` est une méthode de création : Laravel n'enveloppe pas automatiquement le Store retourné dans un Repository.

Cette page a été vérifiée sur l'implémentation de Laravel Framework `v13.39.0`. Pour les opérations de base, consultez [Cache](https://kawax.biz/fr/cache.md), et pour préparer le provider, consultez [Développement de packages](https://kawax.biz/fr/advanced/package-development.md).

## Distinguer le nom du driver du nom du store

| Élément | Exemple de cette page | Rôle |
| - | - | - |
| Nom du driver | `acme-memory` | La méthode de création enregistrée avec `extend()` |
| Nom du store | `catalog` | La configuration définie dans `cache.stores` et l'identifiant du Repository généré |
| Store | `Illuminate\Cache\ArrayStore` | Stockage, lecture et gestion de l'expiration des clés |
| Repository | `Illuminate\Cache\Repository` | `remember()`, conversion du TTL, événements de cache, etc. |

`CacheManager::build()` appelle la création personnalisée correspondant au `driver` de la configuration, si elle existe, avant les drivers intégrés. Ne réutilisez pas des noms comme `redis` ou `array` : utilisez un nom propre au package.

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"catalog déjà résolu ?"}
    B -->|Oui| C["Retourner le Repository conservé"]
    B -->|Non| D["Lire cache.stores.catalog"]
    D --> E["Création pour driver: acme-memory"]
    E --> F["Créer le Store et<br>l'envelopper avec repository()"]
    F --> G["Conserver sous le nom catalog"]
```

## Enregistrer avant le boot des autres providers

Si un autre package utilise le cache dans son `boot()`, enregistrer votre driver dans votre propre `boot()` peut arriver trop tard selon l'ordre de chargement. Conformément à la documentation officielle, enregistrez dans `register()` un **callback `booting` de l'application**.

L'exemple minimal suivant utilise l'`ArrayStore` standard afin de vérifier le chemin d'enregistrement sans service externe. Placez-le dans `src/MemoServiceProvider.php` du package `Acme\Memo`.

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

Chargez le provider, par exemple via la [découverte automatique des packages](https://kawax.biz/fr/advanced/package-discovery.md). Ce provider, qui se contente d'enregistrer une extension, ne doit pas être différé : il doit être chargé aussi bien en HTTP qu'avec Artisan.

* Le `booting` de l'application s'exécute, lors d'un démarrage normal, après la fin du `register()` de chaque provider et avant chaque `boot()`. Il est distinct du `$this->booting()` propre au provider.
* Le callback de `extend()` reçoit l'application et le tableau de configuration du store. Placez la création du Store et les connexions externes dans ce callback de création.
* Ce moment d'enregistrement ne garantit pas **l'utilisation du cache dans le `register()`** d'autres providers. Séparez l'enregistrement des services de leur utilisation effective.

> [!TIP]
>
> Si vous avez seulement besoin du cache en mémoire standard, le driver `array` suffit. Cet exemple sert à expliquer l'enregistrement et les contrats ; il ne permet ni le partage entre processus ni la persistance. Pour un backend propre, remplacez la création de l'`ArrayStore` par une classe qui implémente `Illuminate\Contracts\Cache\Store`.

`CacheManager::extend()` relie à nouveau la portée des closures anonymes au Manager. Ne considérez pas que `$this` dans une closure non statique désigne le provider : utilisez, comme dans l'exemple, une closure statique et l'argument `$app`.

### Ajouter le store dans l'application consommatrice

Ajoutez-le au tableau `stores` existant de `config/cache.php`. Ne concevez pas le package pour réécrire le `default` ou l'ensemble des `stores` de l'application consommatrice.

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

Pour en faire le store par défaut, indiquez dans `CACHE_STORE` le **nom du store `catalog`**, et non le nom du driver. Si vous créez plusieurs configurations avec le même driver, un Repository distinct est généré pour chaque nom de store.

## Construire le Repository soi-même

`CacheManager::callCustomCreator()` retourne tel quel le résultat du callback. Si vous ne retournez que le Store, `get()` ou `put()` peuvent fonctionner, mais les méthodes du Repository comme `remember()` ne seront pas disponibles et l'interprétation du TTL changera également.

Dans l'exemple, `repository($store, $config)` se charge de :

* envelopper le Store dans un `Illuminate\Cache\Repository` ;
* transmettre au Repository le nom `store` que le Manager a ajouté à la configuration ;
* définir le Dispatcher sur le Repository si un Dispatcher d'événements est enregistré et que le paramètre `events` n'est pas désactivé.

Ce traitement n'applique pas toute votre configuration personnalisée au Store. La destination de connexion, le préfixe, la sérialisation, etc. doivent être pris en compte par l'implémentation du Store et par le code de création. C'est aussi le cas de `serialize`, lu depuis la configuration dans l'exemple.

## Confronter le contrat Store à l'implémentation actuelle

Implémentez les méthodes exigées par `Illuminate\Contracts\Cache\Store` dans Laravel 13. Ne distribuez pas tels quels, comme un driver fonctionnel, des listes de méthodes vides issues d'anciens exemples.

| Méthode | Comportement à vérifier |
| - | - |
| `get()` / `many()` | `null` pour une clé absente ou expirée. `many()` retourne un tableau indexé par les clés demandées, avec `null` pour les absentes |
| `put()` / `putMany()` | Stocke avec un TTL en secondes et retourne le succès ou l'échec |
| `forever()` | Stocke sans expiration. Cela n'empêche pas pour autant l'éviction ou la suppression par le backend |
| `touch()` | Modifie l'expiration d'une clé existante. Retourne `false` si la clé est absente ou expirée |
| `increment()` / `decrement()` | Retourne la valeur mise à jour ou un échec. Vérifiez aussi les mises à jour concurrentes et la conservation du TTL |
| `forget()` | Supprime la clé et retourne le résultat |
| `flush()` | Supprime tout le cache. Précisez la portée réellement supprimée |
| `getPrefix()` | Retourne le préfixe de clé utilisé par le Store |

`null` est traité comme un défaut de cache par le Repository. Faites en sorte que `false` ou `0` ne soient pas considérés comme absents. Stocker `null` ne sert pas non plus de marqueur empêchant le recalcul dans `remember()`.

### Vérifier le TTL via le Repository

`Repository::put()` traite le TTL avant de le transmettre au Store.

| Appel au Repository | Traitement côté Store |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | `put()` avec le nombre de secondes |
| `put('key', $value, $expiresAt)` | Conversion d'une date, etc. en secondes, puis `put()` si positif |
| `put('key', $value, 0)` ou TTL négatif | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` ou TTL négatif | `forget()` |

> [!WARNING]
>
> L'`ArrayStore` de l'exemple stocke sans expiration lorsqu'on passe directement `0` seconde à son `put()`. En revanche, `put(..., 0)` sur le Repository supprime l'élément. Tester uniquement le Store de bas niveau obtenu avec `getStore()` ne permet pas de vérifier le comportement de l'API appelée par les utilisateurs.

### Vérifier séparément la prise en charge de add, des verrous et des tags

Implémenter le contrat Store ne garantit pas la prise en charge de toutes les fonctionnalités du cache.

* **`add()` atomique** : il ne fait pas partie du contrat Store. Lorsqu'il est appelé avec un TTL positif et que le Store possède `add()`, le Repository l'utilise. Sinon, il se rabat sur `get()` et `put()` : envelopper le Store dans un Repository ne suffit donc pas à garantir l'atomicité. Sur un backend partagé, implémentez-le avec une opération atomique du backend et vérifiez aussi l'exécution concurrente.
* **Verrous** : `lock()` et `restoreLock()` de `Illuminate\Contracts\Cache\LockProvider` relèvent d'un contrat distinct. Vérifiez l'acquisition, la correspondance du propriétaire, l'expiration et le fait de ne pas libérer le verrou d'un autre propriétaire. Les verrous de l'`ArrayStore` de l'exemple n'assurent pas d'exclusion mutuelle entre processus distincts.
* **Tags** : le Repository vérifie si le Store possède `tags()`. Une implémentation reposant généralement sur `TaggableStore` ou équivalent est nécessaire ; le contrat Store seul ne les active pas automatiquement.

Ne supposez pas que `flush()` est une suppression partielle respectant le préfixe configuré. Vérifiez la portée de la suppression sur un backend partagé et expliquez-la aux utilisateurs, y compris pour `php artisan cache:clear`.

## Distinguer la conservation des instances de la suppression des données

`CacheManager` est un singleton, et il conserve aussi le résultat de `store('catalog')` pour chaque nom de store. Modifier la configuration ou réenregistrer `extend()` ne met pas à jour un Repository déjà généré.

```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` est le même objet que `$before`. C'est lors de la récupération après `forgetDriver()` que `$after` est généré à partir de la nouvelle configuration.

| Opération | Cible |
| - | - |
| `Cache::forgetDriver('catalog')` | Retire la référence au Repository conservée par le Manager |
| `Cache::store('catalog')->forget('key')` | Supprime une clé stockée |
| `Cache::store('catalog')->flush()` | Supprime toutes les données de cache du Store |

`forgetDriver()` ne vide pas le backend. Toutefois, comme l'`ArrayStore` de l'exemple conserve ses données dans l'instance, les anciennes données ne sont pas visibles depuis le nouveau Store. Par ailleurs, les références à l'ancien Repository détenues par `$before` côté consommateur ou par le singleton `cache.store` déjà résolu ne sont pas remplacées.

En production, mettez à jour le cache de configuration selon votre déploiement habituel et redémarrez les workers de longue durée. Tenez aussi compte du [cycle de vie des services](https://kawax.biz/fr/advanced/package-service-lifetimes.md) et ne considérez pas une nouvelle résolution comme une mise à jour simultanée de tous les processus.

## Tester le chemin de création, pas seulement des mocks de façade

Dans l'[environnement de test du package](https://kawax.biz/fr/advanced/package-testing.md), passez par le véritable Manager et le véritable Repository. L'exemple Testbench suivant vérifie que le driver est utilisable même dans le `boot()` d'un autre provider enregistré avant. Il stocke un tableau sans modifier la restriction de désérialisation `cache.serializable_classes`, et vérifie aussi la valeur brute stockée avec `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']);
    }
}
```

Pour un backend propre, vérifiez en plus l'expiration, les clés absentes de `many()`, l'aller-retour de `false` et `0`, `touch()`, les pannes de connexion, les mises à jour concurrentes, ainsi que le préfixe et la portée des suppressions. Un simple succès de `Cache::shouldReceive('get')` ne prouve ni l'enregistrement du driver ni la compatibilité avec un service externe.

## Liste de contrôle pour la maintenance à long terme

* Faire du nom du driver, de la configuration du store, du format de sérialisation, des exceptions et des valeurs de retour en cas d'échec une spécification publique.
* Vérifier, pour chaque version de Laravel prise en charge, les méthodes du contrat Store et les fonctionnalités optionnelles comme les tags et les verrous.
* Vérifier que l'enregistrement fonctionne lors d'un nouveau démarrage avec cache de configuration, et que le driver est utilisable depuis le `boot()` d'autres providers.
* Annoncer les changements de compatibilité des données stockées, y compris la stratégie de migration ou de suppression des clés existantes.
* Distinguer la conservation des instances dans le processus, les données du backend et le cache de configuration.

## Sources primaires consultées

* [Documentation officielle de Laravel : drivers de cache personnalisés](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 : conversion du TTL, add, tags, événements](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Contrat Store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [Contrat LockProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore : stockage, expiration, sérialisation](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider : enregistrement singleton du Manager et de cache.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application : booting de l'application et boot des providers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Tests officiels de 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

- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Cache](https://kawax.biz/fr/cache.md)
- [Contrats](https://kawax.biz/fr/contracts.md)
- [Enregistrement et mise en cache des écouteurs d'événements d'un package](https://kawax.biz/fr/advanced/package-events.md)
- [Créer un agent personnalisé pour Boost](https://kawax.biz/fr/advanced/boost-custom-agent.md)


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