Semua ini kita lakukan dengan kunci tunggal: simpan posts:semua, lupa menghapusnya, dan daftar basi bertahan sampai TTL habis. Semakin banyak kunci, semakin rapuh daftar Cache::forget() manual itu — satu saja terlewat, satu sudut aplikasi menampilkan data lama. Cache tags mengatasinya dengan cara berpikir berbeda: alih-alih menghapus per kunci, kita memberi label kelompok pada entri, lalu menghapus seluruh kelompok sekaligus. Pada chapter ini kita pelajari sintaksnya, driver mana saja yang mendukungnya, dan jalan keluar portable bila driver kita tidak termasuk.

Konsep dan Sintaks #

Method Cache::tags() menerima satu label atau array label, lalu mengembalikan repository bertag yang API-nya sama seperti biasa:

php
use Illuminate\Support\Facades\Cache;

Cache::tags(['produk'])->put('produk:1', 'Laptop', 300);
Cache::tags(['produk', 'unggulan'])->remember('produk:daftar', 300, function () {
    return ['Laptop', 'Mouse'];
});

Cache::tags('produk')->flush(); // hapus SEMUA entri bertag produk

Satu entri boleh membawa beberapa tag sekaligus — entri produk:daftar di atas teriak “saya produk” sekaligus “saya unggulan”, sehingga flush dari kedua sisi mana pun ikut menghapusnya. Tag berperan sebagai namespace untuk kelompok invalidasi: ketika tabel products berubah, kita cukup membuang satu kelompok tanpa perlu tahu kunci apa saja yang pernah ditulisnya.

Dukungan Driver yang Tidak Merata #

Fitur tags ternyata tidak tersedia di semua driver. Mencobanya di store database langsung dijawab exception:

php
use Illuminate\Support\Facades\Cache;

try {
    Cache::tags(['produk'])->put('produk:1', 'Laptop', 300);
} catch (Throwable $e) {
    echo get_class($e) . ': ' . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
BadMethodCallException: This cache store does not support tagging.

Alasannya struktural: menghapus “semua kunci bertag X” menuntut store yang bisa melacak keanggotaan tag secara efisien, dan itu hanya dimungkinkan oleh driver memori tertentu. Kita bisa mengecek dukungannya lewat Cache::store($nama)->supportsTags(). Hasil verifikasi pada Laravel 13.36 di sandbox kita:

DriverMendukung tagsSumber verifikasi
arrayYaDiuji langsung
databaseTidakDiuji langsung (error di atas)
fileTidakDiuji langsung
failoverYaDiuji langsung
redisYaDiuji via supportsTags() (tanpa server)
memcachedYaMewarisi TaggableStore; butuh ekstensi Memcached
apcYaMewarisi TaggableStore
dynamodbTidakKelas DynamoDbStore tanpa method tags() di versi ini

Lima baris teratas diuji langsung lewat supportsTags() di sandbox — store redis ikut teruji karena membangun store-nya tidak membutuhkan koneksi ke server. Tiga sisanya diverifikasi dari kode framework karena membutuhkan ekstensi yang tidak terpasang.

Demo Tanpa Redis: Driver array #

Karena array termasuk store yang mendukung tags, kita bisa merasakan perilakunya tanpa server Redis. Perhatikan pula satu kejutan penting di output berikut:

php
use Illuminate\Support\Facades\Cache;

$tagged = Cache::store('array')->tags(['produk', 'unggulan']);
$tagged->put('produk:1', 'Laptop', 300);
Cache::store('array')->put('lainnya', 'di luar tag', 300);

$viaTag = $tagged->get('produk:1');
$tanpaTag = Cache::store('array')->get('produk:1');

Cache::store('array')->tags('produk')->flush();

echo 'sebelum_flush: via_tag=' . $viaTag
    . ' tanpa_tag=' . var_export($tanpaTag, true) . "\n";
echo 'setelah_flush: produk:1='
    . var_export(Cache::store('array')->get('produk:1'), true)
    . ' lainnya=' . Cache::store('array')->get('lainnya') . "\n";

Output yang Dihasilkan:

text
sebelum_flush: via_tag=Laptop tanpa_tag=NULL
setelah_flush: produk:1=NULL lainnya=di luar tag

Dua fakta terbaca jelas. Pertama, flush() pada tag produk hanya memusnahkan entri bertag — kunci lainnya selamat. Kedua, tanpa_tag=NULL membuktikan entri bertag disimpan di dalam namespace tag, sehingga pembacaan tanpa tag yang sama tidak menemukannya. Konsekuensinya: tulis dan baca harus lewat tag yang sama.

php
$panggilan = 0;
$hitung = function () use (&$panggilan) {
    $panggilan++;

    return 'daftar produk: 2 item';
};

$t = Cache::store('array')->tags('produk');
$t->forget('produk:daftar');
$t->remember('produk:daftar', 300, $hitung);
$t->remember('produk:daftar', 300, $hitung);
$t->flush();
$t->remember('produk:daftar', 300, $hitung);

echo "remember: panggilan_closure={$panggilan}\n";

Output yang Dihasilkan:

text
remember: panggilan_closure=2

Closure dieksekusi sekali untuk dua panggilan beruntun, lalu sekali lagi setelah flush() — siklus hit-miss-invalidasi bekerja dalam kelompok.

Workaround Portable untuk Driver file dan database #

Bagaimana bila production kita memakai database atau file? Kita bisa meniru tags dengan satu kunci indeks yang mencatat daftar kunci per kelompok:

php
use Illuminate\Support\Facades\Cache;

function taggedPut(string $tag, string $key, mixed $value, int $ttl): void
{
    $index = Cache::get("tagindeks:{$tag}", []);
    $index[$key] = true;
    Cache::forever("tagindeks:{$tag}", $index);
    Cache::put($key, $value, $ttl);
}

function taggedFlush(string $tag): int
{
    $index = Cache::get("tagindeks:{$tag}", []);
    foreach (array_keys($index) as $key) {
        Cache::forget($key);
    }
    Cache::forget("tagindeks:{$tag}");

    return count($index);
}

Pengujian di store database membuktikan tiga entri bertag musnah sementara kunci luar tetap utuh:

php
Cache::flush();
taggedPut('produk', 'produk:1', 'Laptop', 300);
taggedPut('produk', 'produk:2', 'Mouse', 300);
taggedPut('produk', 'produk:3', 'Monitor', 300);
Cache::put('statistik:jumlah-user', 3, 300);

$hapus = taggedFlush('produk');
echo 'taggedFlush: kunci_dihapus=' . $hapus
    . ' produk:1=' . var_export(Cache::get('produk:1'), true)
    . ' statistik:jumlah-user=' . Cache::get('statistik:jumlah-user') . "\n";
echo 'indeks_juga_hilang=' . var_export(Cache::get('tagindeks:produk'), true) . "\n";

Output yang Dihasilkan:

text
taggedFlush: kunci_dihapus=3 produk:1=NULL statistik:jumlah-user=3
indeks_juga_hilang=NULL

Pola Penamaan Tag #

Konvensi penamaan menentukan seberapa presisi invalidasi kita. Pola yang umum: satu tag per entity (post:1, post:2) dan satu tag per kelompok daftar (posts). Entri daftar milik pengguna tertentu membawa dua tag sekaligus — posts dan user:7 — sehingga perubahan postingan maupun perubahan data pengguna sama-sama memicu refresh. Prinsipnya sederhana: setiap alasan entri itu basi menjadi satu tag.

Tag memberi kita kelompok penghapusan; satu pertanyaan terakhir tinggal dijawab: kapan sebaiknya penghapusan itu dipicu secara otomatis saat data berubah? Jawabannya — dari observer model sampai pola write-through — adalah inti chapter Cache Invalidation . Daftar resmi method tag tersedia di dokumentasi Cache .