PHPackages                             aftandilmmd/laravel-cacheable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. aftandilmmd/laravel-cacheable

ActiveLibrary[Caching](/categories/caching)

aftandilmmd/laravel-cacheable
=============================

A modern, attribute-driven method caching layer for Laravel. Annotate any method with #\[Cacheable\] and let the framework handle the rest — TTL, tags, conditional caching, locking, stale-while-revalidate, invalidation, and more.

v1.0.2(3w ago)17↓100%MITPHPPHP ^8.2CI passing

Since May 18Pushed 3w agoCompare

[ Source](https://github.com/aftandilmmd/laravel-cacheable)[ Packagist](https://packagist.org/packages/aftandilmmd/laravel-cacheable)[ Docs](https://github.com/aftandilmmd/laravel-cacheable)[ RSS](/packages/aftandilmmd-laravel-cacheable/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (11)Versions (4)Used By (0)

Laravel Cacheable
=================

[](#laravel-cacheable)

[![Tests](https://github.com/aftandilmmd/laravel-cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/aftandilmmd/laravel-cacheable/actions)[![Latest Stable Version](https://camo.githubusercontent.com/22048e7059dac3af9474d4ba9d963c2592f56dbe38a7b702053bfb66d378b447/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f616674616e64696c6d6d642f6c61726176656c2d636163686561626c652e737667)](https://packagist.org/packages/aftandilmmd/laravel-cacheable)[![License](https://camo.githubusercontent.com/cd0846643ac4a76e588a9f27fba6a31568363c58e5390f6edab03f3858bb750d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f616674616e64696c6d6d642f6c61726176656c2d636163686561626c652e737667)](LICENSE.md)[![PHP Version](https://camo.githubusercontent.com/5396cdbc782374af9e0d2d49010612c1fa43971a9cb1eaccc9a2509974b4d469/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f616674616e64696c6d6d642f6c61726176656c2d636163686561626c652e737667)](composer.json)

> **Türkçe:** [README.tr.md](README.tr.md)

Add `#[Cacheable]` to any method. That's it — the package handles TTL, tags, locking, invalidation, and stale-while-revalidate automatically.

```
#[Cacheable(key: 'user.{id}', ttl: 3600, tags: ['users'])]
public function find(int $id): User
{
    return User::findOrFail($id); // only runs on cache miss
}
```

**Requires:** PHP 8.2+ · Laravel 10 / 11 / 12 / 13

---

Installation
------------

[](#installation)

```
composer require aftandilmmd/laravel-cacheable
```

Auto-discovered. No provider or alias registration needed.

Optionally publish the config file:

```
php artisan vendor:publish --tag=cacheable-config
```

---

How it works
------------

[](#how-it-works)

The package intercepts method calls and stores their return values in cache. On the next call with the same arguments, the cached value is returned without executing the method body.

There are two ways to make this interception happen.

---

Real-world example
------------------

[](#real-world-example)

The most natural fit for this package is a **Repository** — a class that owns all database reads for a model. Annotate the read methods, attach `forget` to the write methods, register the repository in `auto_proxy`, and the rest is invisible.

```
// app/Repositories/UserRepository.php

use Aftandilmmd\Cacheable\Attributes\Cacheable;

class UserRepository
{
    // Cache a single user for 1 hour.
    #[Cacheable(key: 'user.{id}', ttl: 3600, tags: ['users'])]
    public function find(int $id): ?User
    {
        return User::find($id);
    }

    // Cache the active user list for 10 minutes.
    #[Cacheable(key: 'users.active', ttl: 600, tags: ['users'])]
    public function allActive(): Collection
    {
        return User::where('active', true)->get();
    }

    // On update: forget this user's entry and flush the list.
    #[Cacheable(forget: ['user.{id}'], forgetTags: ['users'])]
    public function update(int $id, array $data): bool
    {
        return (bool) User::where('id', $id)->update($data);
    }

    // On delete: same invalidation.
    #[Cacheable(forget: ['user.{id}'], forgetTags: ['users'])]
    public function delete(int $id): bool
    {
        return (bool) User::destroy($id);
    }
}
```

Register it once in config:

```
// config/cacheable.php
'auto_proxy' => [
    App\Repositories\UserRepository::class,
],
```

Now every injected instance is automatically cached — no change to controllers or other callers:

```
// app/Http/Controllers/UserController.php

class UserController extends Controller
{
    public function __construct(private UserRepository $users) {}

    public function show(int $id): JsonResponse
    {
        return response()->json($this->users->find($id)); // served from cache
    }

    public function update(int $id, Request $request): JsonResponse
    {
        $this->users->update($id, $request->validated()); // updates DB + clears cache
        return response()->json($this->users->find($id)); // already fresh
    }
}
```

---

Calling cached methods
----------------------

[](#calling-cached-methods)

### Option A — Auto-proxy (recommended)

[](#option-a--auto-proxy-recommended)

Register your service in `config/cacheable.php`. Every container-resolved instance is then automatically wrapped — you call methods normally and caching is invisible.

```
php artisan vendor:publish --tag=cacheable-config
```

```
// config/cacheable.php
'auto_proxy' => [
    App\Services\UserService::class,
],
```

```
// app/Http/Controllers/UserController.php

class UserController extends Controller
{
    public function __construct(private UserService $service) {}

    public function show(int $id): JsonResponse
    {
        return response()->json($this->service->find($id)); // cached
    }
}
```

> **Note:** `new UserService()` bypasses the container and won't be proxied. Always resolve through DI or `app()`.

### Option B — Manual proxy

[](#option-b--manual-proxy)

No config needed. Wrap on the spot, call methods naturally:

```
use Aftandilmmd\Cacheable\Facades\Cacheable;

$service = Cacheable::proxy(new UserService());
$user = $service->find(42); // cached
```

### Option C — Explicit dispatcher

[](#option-c--explicit-dispatcher)

If you prefer explicit over magic, use the `HasCacheableMethods` trait:

```
// app/Services/UserService.php

use Aftandilmmd\Cacheable\Concerns\HasCacheableMethods;

class UserService
{
    use HasCacheableMethods;

    #[Cacheable(key: 'user.{id}', ttl: 3600)]
    public function find(int $id): User { ... }

    #[Cacheable(key: 'users.active', ttl: 600)]
    public static function active(): Collection { ... }
}
```

```
$service->cached('find', [42]); // instance method
$service->cached('active');     // static method — same call
```

> **Self-call limitation:** PHP cannot intercept `$this->method()` inside the same class. Use `$this->cached('method', [...])` for internal calls.

### Static methods via proxy

[](#static-methods-via-proxy)

`CacheableProxy::wrapClass` returns a proxy object that intercepts static calls by name:

```
use Aftandilmmd\Cacheable\Support\CacheableProxy;

$proxy = CacheableProxy::wrapClass(UserService::class);
$proxy->active(); // calls UserService::active() through the cache layer
```

---

Annotating methods
------------------

[](#annotating-methods)

### Basic TTL

[](#basic-ttl)

```
#[Cacheable(ttl: 3600)]
public function all(): Collection { ... }
```

### Key with placeholders

[](#key-with-placeholders)

```
#[Cacheable(key: 'user.{id}.posts', ttl: 600)]
public function posts(int $id): Collection { ... }
```

Placeholders resolve to method argument values. Nested properties also work:

```
#[Cacheable(key: 'order.{order.id}.items', ttl: 600)]
public function items(Order $order): Collection { ... }
```

### Cache forever

[](#cache-forever)

```
#[Cacheable(ttl: null, tags: ['static'])]
public function countries(): array { ... }
```

### Tags

[](#tags)

```
#[Cacheable(ttl: 3600, tags: ['users'])]
public function find(int $id): User { ... }
```

Requires a taggable store (`redis`, `memcached`, `array`). The `file` and `database` drivers silently ignore tags.

### Conditional caching

[](#conditional-caching)

Skip caching based on runtime conditions without changing the call site:

```
#[Cacheable(ttl: 3600, when: 'shouldCache')]
public function expensive(string $type): array { ... }

public function shouldCache(string $type): bool
{
    return $type !== 'realtime';
}
```

`unless` is the inverse — skip cache when the method returns `true`.

### Exclude arguments from the key

[](#exclude-arguments-from-the-key)

Inject heavy objects (Request, Logger) without polluting the cache key:

```
#[Cacheable(key: 'search.{q}', ttl: 300, excludeParams: ['request', 'logger'])]
public function search(string $q, Request $request, LoggerInterface $logger): array { ... }
```

---

Cache invalidation
------------------

[](#cache-invalidation)

### On write methods

[](#on-write-methods)

Attach `forget` or `forgetTags` to a mutating method. Cache entries are deleted automatically after the method runs:

```
#[Cacheable(forget: ['user.{id}'], forgetTags: ['users'])]
public function update(int $id, array $data): bool
{
    return User::find($id)->update($data);
}
```

### Via facade

[](#via-facade)

Manually delete a specific entry or flush a tag group:

```
use Aftandilmmd\Cacheable\Facades\Cacheable;

Cacheable::forget(UserService::class, 'find', [42]);
Cacheable::flushTags(['users']);
```

### Version bump

[](#version-bump)

Bump the `version` parameter to invalidate everything at once without touching the cache store:

```
// config/cacheable.php
'version' => env('CACHEABLE_VERSION', 'v2'), // was v1
```

Or per attribute:

```
#[Cacheable(key: 'user.{id}', ttl: 3600, version: 'v2')]
```

---

Stampede protection
-------------------

[](#stampede-protection)

When many concurrent requests miss the same key, only one should hit the database. Use a distributed lock:

```
#[Cacheable(key: 'report.{date}', ttl: 3600, lock: true, lockWait: 15)]
public function heavyReport(string $date): array { ... }
```

To spread expiration times across many keys and avoid simultaneous cache misses, add jitter:

```
#[Cacheable(ttl: 3600, jitter: 300)] // effective TTL: 3600–3900 seconds
public function popular(): array { ... }
```

---

Stale-while-revalidate
----------------------

[](#stale-while-revalidate)

Serve cached data immediately while refreshing in the background. `refreshAhead: 0.2` means "trigger a refresh in the final 20% of the TTL window":

```
#[Cacheable(key: 'dashboard', ttl: 600, refreshAhead: 0.2)]
public function dashboardStats(): array { ... }
```

For async refresh via the queue, enable it in config:

```
// config/cacheable.php
'swr' => [
    'async'            => true,
    'queue_connection' => 'redis',
    'queue_name'       => 'cache',
],
```

---

Events
------

[](#events)

Listen in `app/Providers/AppServiceProvider.php`:

EventFires whenProperties`CacheHit`Cached value returned`key`, `class`, `method`, `value``CacheMissed`No cache — method will run`key`, `class`, `method``CacheWritten`Result stored in cache`key`, `class`, `method`, `value`, `ttl``CacheForgotten`Keys/tags invalidated`class`, `method`, `keys`, `tags````
// app/Providers/AppServiceProvider.php

use Aftandilmmd\Cacheable\Events\CacheHit;
use Aftandilmmd\Cacheable\Events\CacheMissed;
use Aftandilmmd\Cacheable\Events\CacheWritten;
use Aftandilmmd\Cacheable\Events\CacheForgotten;

Event::listen(CacheHit::class, function (CacheHit $event) {
    Log::debug('Cache HIT', ['key' => $event->key, 'method' => $event->method]);
});

Event::listen(CacheMissed::class, function (CacheMissed $event) {
    Log::debug('Cache MISS', ['key' => $event->key, 'method' => $event->method]);
});

Event::listen(CacheWritten::class, function (CacheWritten $event) {
    Log::debug('Cache WRITTEN', ['key' => $event->key, 'ttl' => $event->ttl]);
});

Event::listen(CacheForgotten::class, function (CacheForgotten $event) {
    Log::debug('Cache FORGOTTEN', ['keys' => $event->keys, 'tags' => $event->tags]);
});
```

Disable all events globally: set `cacheable.events.enabled = false` in config.

---

Debugging
---------

[](#debugging)

```
use Aftandilmmd\Cacheable\Facades\Cacheable;

// See exactly which key would be generated for a call
$key = Cacheable::keyFor(UserService::class, 'find', [42]);
// → "cacheable:v1:App\Services\UserService:find:a1b2c3..."

// Delete the cached value and immediately re-execute the method to warm cache
Cacheable::refresh(UserService::class, 'find', [42]);
```

If the key looks like a hash, set an explicit `key:` template in the attribute for human-readable keys.

---

Configuration
-------------

[](#configuration)

Publish and edit `config/cacheable.php` to set global defaults. Every attribute parameter that accepts `null` falls back to these values.

```
return [
    'enabled' => env('CACHEABLE_ENABLED', true),
    'store'   => env('CACHEABLE_STORE'),           // null = Laravel default
    'prefix'  => env('CACHEABLE_PREFIX', 'cacheable'),
    'version' => env('CACHEABLE_VERSION', 'v1'),
    'ttl'     => env('CACHEABLE_TTL', 3600),

    'keys' => [
        'hash_algo'  => 'xxh128',
        'serializer' => 'json',   // 'json' | 'serialize' | 'igbinary'
        'max_length' => 200,
    ],

    'stampede' => [
        'jitter'    => 0,
        'lock_wait' => 10,
    ],

    'swr' => [
        'refresh_ahead'    => 0.0,
        'async'            => false,
        'queue_connection' => env('CACHEABLE_SWR_CONNECTION'),
        'queue_name'       => env('CACHEABLE_SWR_QUEUE', 'default'),
    ],

    'storage' => [
        'cache_null'  => false,
        'cache_empty' => true,
    ],

    'events' => [
        'enabled' => true,
    ],

    'auto_proxy' => [
        // App\Services\UserService::class,
    ],
];
```

---

Attribute reference
-------------------

[](#attribute-reference)

All parameters are optional. Omitting a parameter uses the config default.

ParameterTypeDescription`key``?string`Key template. Supports `{param}` and `{param.property}` placeholders. Auto-generated when null.`prefix``?string`Prepended to the key.`ttl``?int`Seconds. `null` = cache forever.`tags``string[]`Tag groups. Requires taggable store.`store``?string`Override cache store.`keyParams``string[]`Whitelist: only these params are used in key generation.`excludeParams``string[]`Blacklist: these params are excluded from key generation.`when``?string`Method name on `$this` → cache only when it returns `true`.`unless``?string`Method name on `$this` → skip cache when it returns `true`.`cacheNull``?bool`Store `null` return values.`cacheEmpty``?bool`Store empty arrays / strings / Collections.`lock``bool`Enable distributed lock for stampede protection.`lockWait``?int`Seconds to wait for lock.`jitter``?int`Random seconds added to TTL.`refreshAhead``?float``0..1` — fraction of TTL at which to trigger refresh.`forget``string[]`Key templates to delete after this method runs.`forgetTags``string[]`Tags to flush after this method runs.`version``?string`Embedded in key. Bump to invalidate all entries.`hashAlgo``?string`Hash algorithm for auto-generated keys.`serializer``?string`Argument serializer: `json` / `serialize` / `igbinary`.---

Extending
---------

[](#extending)

### Custom key resolver

[](#custom-key-resolver)

```
// app/Services/TenantAwareKeyResolver.php

use Aftandilmmd\Cacheable\Contracts\KeyResolver;

class TenantAwareKeyResolver implements KeyResolver
{
    public function __construct(private KeyResolver $inner) {}

    public function resolve(...$args): string
    {
        return tenant()->id . ':' . $this->inner->resolve(...$args);
    }
}
```

```
// app/Providers/AppServiceProvider.php

$this->app->extend(KeyResolver::class, fn ($inner) =>
    new TenantAwareKeyResolver($inner)
);
```

### Custom argument normalizer

[](#custom-argument-normalizer)

```
// app/Providers/AppServiceProvider.php

$this->app->singleton(ArgumentNormalizer::class, MyNormalizer::class);
```

You can also swap the entire caching pipeline by implementing `CacheAspect`.

---

Troubleshooting
---------------

[](#troubleshooting)

**`$this->method()` isn't cached.**PHP can't intercept self-calls. Use `$this->cached('method', [...])` or inject a proxy.

**Static method isn't cached.**`auto_proxy` and `Cacheable::proxy()` only wrap instances. Use `HasCacheableMethods` + `cached('method', [...])` or `CacheableProxy::wrapClass(MyClass::class)->method()`.

**Tags do nothing.**Tags require a taggable store (`redis`, `memcached`, `array`). Switch the store or use `forget` keys instead.

**Cache isn't cleared between tests.**Add `Cache::flush()` to your test's `setUp()`.

---

License
-------

[](#license)

MIT © Aftandilmmd. See [LICENSE.md](LICENSE.md).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance95

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

22d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/630ea45c41a7b5c54294a46920d0036cf0693865c7d3254c445b6cc08670edd0?d=identicon)[aftandilmmd](/maintainers/aftandilmmd)

---

Top Contributors

[![aftandilmmd](https://avatars.githubusercontent.com/u/26919900?v=4)](https://github.com/aftandilmmd "aftandilmmd (4 commits)")

---

Tags

laravelcachestale-while-revalidateannotationcacheablephp8memoizationattributestampede

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/aftandilmmd-laravel-cacheable/health.svg)

```
[![Health](https://phpackages.com/badges/aftandilmmd-laravel-cacheable/health.svg)](https://phpackages.com/packages/aftandilmmd-laravel-cacheable)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[spatie/laravel-responsecache

Speed up a Laravel application by caching the entire response

2.8k8.7M64](/packages/spatie-laravel-responsecache)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9732.3M121](/packages/roots-acorn)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k14.1M120](/packages/laravel-pulse)[propaganistas/laravel-disposable-email

Disposable email validator

6012.9M7](/packages/propaganistas-laravel-disposable-email)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
