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

# Store di cache personalizzati e contratti nei pacchetti

> Analizzare CacheManager e il contratto Store di Laravel 13 e verificare il momento della registrazione del driver, la conversione del TTL tramite Repository, la conservazione delle istanze e il supporto a funzionalità come i lock.

Per fornire un backend di cache personalizzato da un pacchetto, separa **lo Store che implementa il salvataggio** dal **Repository che espone le API di Laravel**. Ciò che registri con `Cache::extend()` è il modo di creare lo store: Laravel non avvolge automaticamente in un Repository lo Store che restituisci.

Questa pagina esamina l'implementazione di Laravel Framework `v13.39.0`. Per le operazioni di base consulta [Cache](https://kawax.biz/it/cache.md), per la preparazione del provider consulta [Sviluppo di pacchetti](https://kawax.biz/it/advanced/package-development.md).

## Separare il nome del driver dal nome dello store

| Elemento | Esempio in questa pagina | Ruolo |
| - | - | - |
| Nome del driver | `acme-memory` | Il metodo di creazione registrato con `extend()` |
| Nome dello store | `catalog` | La configurazione definita in `cache.stores` e l'identificatore del Repository già creato |
| Store | `Illuminate\Cache\ArrayStore` | Salvataggio e lettura delle chiavi, gestione della scadenza |
| Repository | `Illuminate\Cache\Repository` | `remember()`, conversione del TTL, eventi della cache e così via |

Se esiste una callback di creazione personalizzata corrispondente al `driver` della configurazione, `CacheManager::build()` la invoca prima dei driver integrati. Non riutilizzare nomi come `redis` o `array`: usa un nome specifico del pacchetto.

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"catalog è già stato risolto?"}
    B -->|Sì| C["Restituisce il Repository conservato"]
    B -->|No| D["Legge cache.stores.catalog"]
    D --> E["Callback di creazione per driver: acme-memory"]
    E --> F["Crea lo Store e<br>lo avvolge con repository()"]
    F --> G["Lo conserva con il nome catalog"]
```

## Registrare prima del boot degli altri provider

Se un altro pacchetto usa la cache nel proprio `boot()`, registrare il driver nel tuo `boot()` potrebbe non arrivare in tempo, a seconda dell'ordine. Seguendo la documentazione ufficiale, in `register()` registra una **callback `booting` dell'applicazione**.

L'esempio seguente è minimo e usa l'`ArrayStore` standard per verificare il percorso di registrazione senza servizi esterni. Collocalo in `src/MemoServiceProvider.php` del pacchetto `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);
            });
        });
    }
}
```

Il provider viene caricato, ad esempio, tramite il [package auto-discovery](https://kawax.biz/it/advanced/package-discovery.md). Questo provider, che si limita a registrare l'estensione, non deve essere un provider differito, così da essere caricato sia in HTTP sia in Artisan.

* Nel normale avvio, la callback `booting` dell'applicazione viene eseguita dopo il completamento di `register()` di tutti i provider e prima di ciascun `boot()`. È diversa da `$this->booting()` del provider stesso.
* Alla callback di `extend()` vengono passati l'applicazione e l'array di configurazione dello store. Colloca la creazione dello Store e le connessioni esterne in questa callback di creazione.
* Questo momento di registrazione non garantisce il funzionamento per **l'uso della cache all'interno di `register()`** di altri provider. Separa la registrazione dei servizi dal loro utilizzo effettivo.

> [!TIP]
>
> Se ti serve solo la cache in memoria standard, il driver `array` è sufficiente. Questo esempio serve a illustrare la registrazione e i contratti, e non può essere usato per condivisione tra processi o persistenza. Per un backend personalizzato, sostituisci la creazione di `ArrayStore` con una classe che implementa `Illuminate\Contracts\Cache\Store`.

`CacheManager::extend()` ricollega lo scope delle closure anonime al Manager. Non considerare `$this` di una closure non static come il provider: come nell'esempio, usa closure static e l'argomento `$app`.

### Aggiungere lo store nell'applicazione che usa il pacchetto

Aggiungilo all'array `stores` esistente in `config/cache.php`. Non progettare il pacchetto in modo che sovrascriva il `default` o l'intero `stores` dell'applicazione.

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

Anche quando vuoi renderlo predefinito, in `CACHE_STORE` indichi **il nome dello store `catalog`**, non il nome del driver. Se crei più configurazioni con lo stesso driver, viene generato un Repository distinto per ogni nome di store.

## Costruire il Repository da sé

`CacheManager::callCustomCreator()` restituisce così com'è il valore restituito dalla callback. Se restituisci solo lo Store, anche se `get()` e `put()` funzionano, metodi del Repository come `remember()` non sono disponibili e anche l'interpretazione del TTL cambia.

Nell'esempio, `repository($store, $config)` si occupa di quanto segue.

* Avvolge lo Store con `Illuminate\Cache\Repository`.
* Passa al Repository il nome `store` che il Manager ha aggiunto alla configurazione.
* Se è registrato un Dispatcher di eventi e l'opzione `events` non è disabilitata, imposta il Dispatcher sul Repository.

Non si tratta di un'operazione che applica allo Store tutte le opzioni personalizzate. Destinazione della connessione, prefix, serializzazione e così via vanno applicati nell'implementazione dello Store e nella callback di creazione. Lo stesso vale per `serialize`, letto dalla configurazione nell'esempio.

## Confrontare il contratto Store con l'implementazione attuale

Implementa i metodi richiesti da `Illuminate\Contracts\Cache\Store` in Laravel 13. Non distribuire come driver funzionante un elenco di metodi vuoti preso da esempi passati.

| Metodo | Comportamento da verificare |
| - | - |
| `get()` / `many()` | Restituisce `null` per chiavi mancanti o scadute. `many()` restituisce un array con le chiavi richieste come chiavi, anche quelle mancanti con valore `null` |
| `put()` / `putMany()` | Salva con un TTL in secondi e restituisce successo o fallimento |
| `forever()` | Salva senza scadenza. Non significa impedire l'eviction o la cancellazione da parte del backend |
| `touch()` | Modifica la scadenza di una chiave esistente. Restituisce `false` se la chiave è mancante o scaduta |
| `increment()` / `decrement()` | Restituisce il valore aggiornato o il fallimento. Verifica anche gli aggiornamenti concorrenti e il mantenimento del TTL |
| `forget()` | Elimina la chiave e restituisce il risultato |
| `flush()` | Elimina l'intera cache. Indica chiaramente l'ambito effettivamente cancellato |
| `getPrefix()` | Restituisce il prefisso delle chiavi usato dallo Store |

Il Repository tratta `null` come cache miss. L'implementazione non deve trattare come mancanti anche `false` o `0`. Salvare `null` non funziona come marcatore per evitare il ricalcolo di `remember()`.

### Verificare il TTL passando per il Repository

`Repository::put()` elabora il TTL prima di passarlo allo Store.

| Chiamata al Repository | Operazione sullo Store |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | `put()` con i secondi |
| `put('key', $value, $expiresAt)` | Converte data/ora o simili in secondi e, se positivo, `put()` |
| `put('key', $value, 0)` o TTL negativo | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` o TTL negativo | `forget()` |

> [!WARNING]
>
> L'`ArrayStore` dell'esempio, se riceve direttamente `0` secondi in `put()`, salva senza scadenza. Invece `put(..., 0)` del Repository elimina la chiave. Testando solo lo Store di basso livello ottenuto con `getStore()` non verifichi il comportamento delle API chiamate dagli utenti.

### Verificare separatamente il supporto ad add, lock e tag

La sola implementazione del contratto Store non garantisce il supporto a tutte le funzionalità della cache.

* **`add()` atomico**: non fa parte del contratto Store. Quando viene chiamato con un TTL positivo e lo Store ha `add()`, il Repository lo usa. Altrimenti ricorre a un fallback con `get()` e `put()`, quindi il solo wrapping nel Repository non garantisce l'atomicità. Con un backend condiviso, implementalo con un'operazione atomica di quel backend e verifica anche l'esecuzione concorrente.
* **Lock**: `lock()` e `restoreLock()` di `Illuminate\Contracts\Cache\LockProvider` sono un contratto separato. Verifica acquisizione, corrispondenza del proprietario, scadenza e il fatto che non venga rilasciato il lock di un altro proprietario. I lock di `ArrayStore` dell'esempio non forniscono mutua esclusione tra processi diversi.
* **Tag**: il Repository controlla se lo Store ha `tags()`. In generale serve un'implementazione basata su `TaggableStore` o simili: il contratto Store da solo non li abilita automaticamente.

Non considerare `flush()` come una cancellazione parziale che rispetta il prefix della configurazione. Verifica l'ambito della cancellazione sul backend condiviso e spiegalo agli utenti, incluso `php artisan cache:clear`.

## Distinguere la conservazione delle istanze dalla cancellazione dei dati

`CacheManager` è un singleton e conserva anche il risultato di `store('catalog')` per ogni nome di store. La sola modifica della configurazione o una nuova registrazione con `extend()` non aggiorna i Repository già creati.

```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` è lo stesso oggetto di `$before`. Con il recupero successivo a `forgetDriver()`, `$after` viene creato dalla nuova configurazione.

| Operazione | Oggetto |
| - | - |
| `Cache::forgetDriver('catalog')` | Rimuove il riferimento al Repository conservato dal Manager |
| `Cache::store('catalog')->forget('key')` | Elimina una singola chiave salvata |
| `Cache::store('catalog')->flush()` | Elimina tutti i dati della cache dello Store |

`forgetDriver()` non cancella tutto il backend. Tuttavia, poiché l'`ArrayStore` dell'esempio conserva i dati all'interno dell'istanza, i vecchi dati non sono visibili dal nuovo Store. Inoltre, i riferimenti al vecchio Repository conservati da `$before` lato chiamante o dal singleton `cache.store` già risolto non vengono sostituiti.

In produzione, aggiorna la cache della configurazione seguendo il normale deploy e riavvia i worker di lunga durata. Tieni conto anche del [ciclo di vita dei servizi](https://kawax.biz/it/advanced/package-service-lifetimes.md) e non trattare una nuova risoluzione come un aggiornamento simultaneo di tutti i processi.

## Testare il percorso di creazione, non solo il mock della facade

Nell'[ambiente di test dei pacchetti](https://kawax.biz/it/advanced/package-testing.md), passa per il Manager e il Repository reali. Il seguente esempio con Testbench verifica che il driver sia utilizzabile anche nel `boot()` di un altro provider registrato prima. Nell'esempio si salva un array senza modificare la restrizione di deserializzazione `cache.serializable_classes` e si controlla anche il valore grezzo salvato con `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']);
    }
}
```

Per un backend personalizzato, verifica inoltre la scadenza, le chiavi mancanti in `many()`, l'andata e ritorno di `false` e `0`, `touch()`, i guasti di connessione, gli aggiornamenti concorrenti, il prefix e l'ambito della cancellazione. Il semplice successo di `Cache::shouldReceive('get')` non è una prova della registrazione del driver né della compatibilità con il servizio esterno.

## Checklist per la manutenzione a lungo termine

* Rendi parte della specifica pubblica il nome del driver, la configurazione dello store, il formato di serializzazione, le eccezioni e i valori restituiti in caso di fallimento.
* Verifica i metodi del contratto Store e le funzionalità opzionali come tag e lock per ogni versione di Laravel supportata.
* Verifica che il driver venga registrato anche in un avvio pulito con la cache della configurazione attiva e che sia utilizzabile dal `boot()` di altri provider.
* Quando modifichi la compatibilità dei dati salvati, comunicalo includendo la strategia di migrazione o eliminazione delle chiavi esistenti.
* Distingui la conservazione delle istanze nel processo, i dati del backend e la cache della configurazione.

## Fonti primarie consultate

* [Documentazione ufficiale di Laravel: driver di cache personalizzati](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: conversione del TTL, add, tags, eventi](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Contratto Store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [Contratto LockProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore: salvataggio, scadenza, serializzazione](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider: registrazione singleton del Manager e di cache.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application: booting dell'applicazione e boot dei provider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Test ufficiali di 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

- [Argomenti avanzati](https://kawax.biz/it/advanced/index.md)
- [Cache](https://kawax.biz/it/cache.md)
- [Sovrascrivere e aggiornare le view di un pacchetto](https://kawax.biz/it/advanced/package-views.md)
- [Contracts (contratti)](https://kawax.biz/it/contracts.md)
- [Provider personalizzati](https://kawax.biz/it/packages/laravel-copilot-sdk/custom-providers.md)


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