Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6338f46
fix(cache): preserve fixed-width file expiration headers
binaryfire Jul 26, 2026
937982c
feat(cache): add the storage cache driver
binaryfire Jul 26, 2026
4050d99
feat(cache): complete cache driver configuration
binaryfire Jul 26, 2026
b278e16
feat(cache): complete repository parity and value handling
binaryfire Jul 26, 2026
124af31
fix(cache): preserve callback failures during lock release
binaryfire Jul 26, 2026
c46cd72
fix(cache): preserve callback failures during limiter release
binaryfire Jul 26, 2026
1f062f1
fix(cache): expire custom rate limiter increments
binaryfire Jul 26, 2026
5707852
feat(cache): make failover lock cleanup truthful
binaryfire Jul 26, 2026
7a6bd8d
fix(cache): propagate lock cleanup through wrapped stores
binaryfire Jul 26, 2026
f57a99b
fix(cache): make stack cleanup exhaustive
binaryfire Jul 26, 2026
481c1c0
fix(cache): preserve Swoole cache permanence and serialization
binaryfire Jul 26, 2026
e3cf14e
fix(cache): make Swoole timer startup transactional
binaryfire Jul 26, 2026
5bc33e3
fix(cache): restore tagged cache event semantics
binaryfire Jul 26, 2026
7c85194
perf(cache): release Redis leases around cache callbacks
binaryfire Jul 26, 2026
7cba533
perf(cache): bound Redis tag maintenance leases
binaryfire Jul 26, 2026
c89a27f
fix(cache): support permanent any-mode additions
binaryfire Jul 26, 2026
abb3707
refactor(cache): remove dead Redis command config defaults
binaryfire Jul 26, 2026
b76118f
docs(cache): document current cache APIs and behavior
binaryfire Jul 26, 2026
8b07447
docs(cache): document safe object serialization boundaries
binaryfire Jul 26, 2026
11bb30f
test(cache): guard RedisStore cache invalidation
binaryfire Jul 26, 2026
c066cbd
docs(boost): correct documentation website link
binaryfire Jul 26, 2026
7bb972b
docs(audit): complete cache lifecycle work unit
binaryfire Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -990,8 +990,8 @@ An exceptionally large shared work unit may receive its own linked detail plan w

This compact index routes the completed-work history that must be consulted with the full plan after compaction. Detailed history remains in the [companion ledger](2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md).

- **Active package or work unit:** `cache`
- **Ledger entries required for the active work:** `Coordinate shared container construction and complete current contextual resolution`; `Normalize framework enum identifiers at string boundaries`; `Harden filesystem I/O, streaming, and response teardown`; `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`.
- **Active package or work unit:** `session`
- **Ledger entries required for the active work:** `Normalize framework enum identifiers at string boundaries`; `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`; `Complete Cache parity, cleanup, permanence, and tagged ownership`.
- **Pending revalidation carried into the active work:** None.

Update these three lines when a package starts, completes, or gains a cross-package dependency. Name exact work-unit headings or shared finding IDs from the companion ledger; never use “see recent entries” or require a full-ledger reread.
Expand Down Expand Up @@ -1100,6 +1100,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano
| `telescope-01` | `telescope` | `redis` (revalidation complete); later full `telescope` audit | `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`; finding `telescope-01` |
| `telescope-02` | `telescope` | `redis` (revalidation complete); later full `telescope` audit | `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`; finding `telescope-02` |
| `sentry-01` | `sentry` | `redis` (revalidation complete); later full `sentry` audit | `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`; finding `sentry-01` |
| `cache-04` | `cache` | `auth`, `sanctum`, and `testbench` (revalidation complete); later full consumer audits | `Complete Cache parity, cleanup, permanence, and tagged ownership`; finding `cache-04` |

## Package checklist

Expand Down Expand Up @@ -1170,7 +1171,7 @@ The order is lower-level first where practical. Hypervel has cross-cutting depen

- [x] `database`
- [x] `redis`
- [ ] `cache`
- [x] `cache`
- [ ] `session`
- [ ] `queue`
- [ ] `horizon`
Expand Down

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ Per-provider config in `config/auth.php`:
],
```

Allow every configured user model to be unserialized in `config/cache.php`:

```php
'serializable_classes' => [
App\Models\User::class,
],
```

For supported stores and Redis serializer requirements, see [Serializable Cached Objects](https://hypervel.org/docs/cache#serializable-cached-objects).

Minimum env setup for single Redis node:

```env
Expand Down Expand Up @@ -279,6 +289,8 @@ Cache::store('auth')->tags(['tenant:5'])->flush(); // just tenant 5's users

### Threat model

Keep `cache.serializable_classes` limited to the user model classes this cache needs. Broad allowlists expand PHP's unserialization surface.

For auth-sensitive contexts (admin panels, financial actions), consider:

- Shorter L1 TTL (1–2s) — still absorbs bursts, narrower staleness window
Expand Down
2 changes: 1 addition & 1 deletion src/boost/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Boost for Hypervel

# Hypervel Documentation

You can find the online version of the Hypervel documentation at [https://hypervel.com/docs](https://hypervel.org/docs)
You can find the online version of the Hypervel documentation at [https://hypervel.org/docs](https://hypervel.org/docs)

## Contribution Guidelines

Expand Down
10 changes: 10 additions & 0 deletions src/boost/docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ You may enable the cache per Eloquent provider in your application's `config/aut
],
```

Because the supported stores serialize cached user models, add every configured user model to the `serializable_classes` option in your application's `config/cache.php` file:

```php
'serializable_classes' => [
App\Models\User::class,
],
```

Keep the allowlist limited to the user model classes your authentication cache needs. For supported stores and Redis serializer requirements, see [Serializable Cached Objects](/docs/{{version}}/cache#serializable-cached-objects).

When `store` is `null`, Hypervel uses your default cache store. For a single Redis-backed deployment, you may enable the cache like this:

```ini
Expand Down
56 changes: 53 additions & 3 deletions src/boost/docs/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Introduction](#introduction)
- [Configuration](#configuration)
- [Array Cache Stores](#array-cache-stores)
- [Serializable Cached Objects](#serializable-cached-objects)
- [Driver Prerequisites](#driver-prerequisites)
- [Swoole Table Cache](#swoole-table-cache)
- [Building Cache Stacks](#building-cache-stacks)
Expand Down Expand Up @@ -47,7 +48,7 @@ Thankfully, Hypervel provides an expressive, unified API for various cache backe
<a name="configuration"></a>
## Configuration

Your application's cache configuration file is located at `config/cache.php`. In this file, you may specify which cache store you would like to be used by default throughout your application. Hypervel supports Redis, relational databases, file storage, Swoole tables, session storage, cache stacks, failover stores, and the `array`, `worker-array`, and `null` stores that are convenient for automated tests and in-memory cache data.
Your application's cache configuration file is located at `config/cache.php`. In this file, you may specify which cache store you would like to be used by default throughout your application. Hypervel supports Redis, relational databases, local files, filesystem disks, Swoole tables, session storage, cache stacks, failover stores, and the `array`, `worker-array`, and `null` stores that are convenient for automated tests and in-memory cache data.

The cache configuration file also contains a variety of other options that you may review. By default, Hypervel is configured to use the `database` cache driver, which stores serialized cache values in your application's database.

Expand Down Expand Up @@ -78,6 +79,31 @@ Use `array` for request-local test and scratch data. Use `worker-array` only whe
],
```

<a name="serializable-cached-objects"></a>
### Serializable Cached Objects

By default, Hypervel cache stores that serialize values do not instantiate PHP classes while unserializing them. If your application intentionally caches objects, list the classes that may be unserialized using the `serializable_classes` option:

```php
'serializable_classes' => [
App\Data\UserProfile::class,
],
```

This applies to database, file, Redis, storage, and Swoole stores, as well as stacks containing them. Array and worker-array stores configured with `'serialize' => false` keep values in memory and do not use this setting.

You may register a callback during application boot to observe cached objects whose classes are not allowed or available:

```php
use Hypervel\Support\Facades\Cache;

Cache::handleUnserializableClassUsing(function (string $key, ?string $class) {
// ...
});
```

If you use Redis and rely on this allowlist, configure the connection used by your cache store with the default `Redis::SERIALIZER_NONE` serializer. Native PhpRedis serializers handle cached values before Hypervel can apply the allowlist; `Redis::SERIALIZER_PHP` and `Redis::SERIALIZER_IGBINARY` may instantiate PHP classes when reading cached values. With a native serializer, the callback may still report unavailable classes, but it cannot report classes excluded by the allowlist. Other Redis connections may continue using any supported serializer.

<a name="driver-prerequisites"></a>
### Driver Prerequisites

Expand Down Expand Up @@ -114,6 +140,19 @@ You may run the `cache:redis-doctor` command to verify your Redis cache configur
php artisan cache:redis-doctor
```

<a name="storage"></a>
#### Storage

The `storage` cache driver allows you to store cached values on any configured [filesystem disk](/docs/{{version}}/filesystem). This can be useful when you want to use an existing disk, such as an S3 disk, as a key / value cache store:

```php
'storage' => [
'driver' => 'storage',
'disk' => env('CACHE_STORAGE_DISK'),
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],
```

<a name="swoole-table-cache"></a>
### Swoole Table Cache

Expand Down Expand Up @@ -316,6 +355,14 @@ $value = Cache::remember('users', $seconds, function () {

If the item does not exist in the cache, the closure passed to the `remember` method will be executed and its result will be placed in the cache.

If you need to know whether the item was retrieved from the cache instead of by executing the given closure, you may use the `rememberWithWarmth` method. This method returns an array containing the cached value and a boolean indicating whether the item was "warm", meaning it was retrieved from the cache and not resolved from the closure:

```php
[$value, $warm] = Cache::rememberWithWarmth('users', $seconds, function () {
return DB::table('users')->get();
});
```

You may use the `rememberForever` method to retrieve an item from the cache or store it forever if it does not exist:

```php
Expand Down Expand Up @@ -432,7 +479,7 @@ Cache::forever('key', 'value');
```

> [!NOTE]
> If you are using the `swoole` driver, items stored using the `forever` method are stored with a long expiration time and may still expire or be evicted when the table reaches its capacity, depending on the configured eviction policy.
> If you are using the `swoole` driver, items stored using the `forever` method do not expire based on time, but may still be evicted when the table reaches its capacity, depending on the configured eviction policy.

<a name="removing-items-from-the-cache"></a>
### Removing Items From the Cache
Expand Down Expand Up @@ -538,7 +585,7 @@ cache()->remember('users', $seconds, function () {
## Cache Tags

> [!WARNING]
> Cache tags are supported by the `redis`, `array`, `failover`, `null`, and `stack` cache drivers. Stack tags require an any-mode composition; see [Tagged Cache Stacks](#tagged-cache-stacks). Cache tags are not supported by the `file`, `database`, `swoole`, `session`, or `memo` drivers.
> Cache tags are supported by the `redis`, `array`, `failover`, `null`, and `stack` cache drivers. Stack tags require an any-mode composition; see [Tagged Cache Stacks](#tagged-cache-stacks). Cache tags are not supported by the `file`, `storage`, `database`, `swoole`, `session`, or `memo` drivers.

<a name="redis-tag-modes"></a>
### Redis Tag Modes
Expand Down Expand Up @@ -1047,6 +1094,9 @@ Hypervel includes several Artisan commands for working with cache stores:

To execute code on every cache operation, you may listen for various [events](/docs/{{version}}/events) dispatched by the cache:

> [!NOTE]
> Cache additions handled by a store's native atomic `add` operation do not dispatch cache events. Redis `any` tag mode performs `add` atomically with or without a time-to-live.

<div class="overflow-auto">

| Event Name |
Expand Down
2 changes: 2 additions & 0 deletions src/boost/docs/redis.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ The PhpRedis extension may also be configured to use a variety of serializers an

Currently supported serializers include: `Redis::SERIALIZER_NONE` (default), `Redis::SERIALIZER_PHP`, `Redis::SERIALIZER_JSON`, `Redis::SERIALIZER_IGBINARY`, and `Redis::SERIALIZER_MSGPACK`.

When relying on Cache's [serializable class allowlist](/docs/{{version}}/cache#serializable-cached-objects), configure the connection used by the Redis cache store with `Redis::SERIALIZER_NONE`. Options in the shared `options` array also apply to that connection.

Supported compression algorithms include: `Redis::COMPRESSION_NONE` (default), `Redis::COMPRESSION_LZF`, `Redis::COMPRESSION_ZSTD`, and `Redis::COMPRESSION_LZ4`.

<a name="clusters"></a>
Expand Down
11 changes: 11 additions & 0 deletions src/boost/docs/sanctum.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ Token caching is disabled by default. You may enable and configure it in your ap
],
```

Because token caching stores both personal access token and tokenable model objects, add the configured model classes to the `serializable_classes` option in your application's `config/cache.php` file:

```php
'serializable_classes' => [
App\Models\User::class,
Hypervel\Sanctum\PersonalAccessToken::class,
],
```

If you use a custom personal access token model, list it instead of the default model. For supported stores and Redis serializer requirements, see [Serializable Cached Objects](/docs/{{version}}/cache#serializable-cached-objects).

The `store` option determines which cache store is used. When this value is `null`, Sanctum uses your application's default cache store. The `ttl` option controls how long token and tokenable entries remain cached, in seconds. The `prefix` option is prepended to Sanctum's cache keys.

Sanctum also caches missing token IDs and missing tokenable models as `null` results for the configured TTL. This protects your database from repeated lookups for the same revoked, deleted, or orphaned token data. Because token IDs come from request input, use a cache store with bounded memory or an eviction policy when enabling token caching on public endpoints.
Expand Down
59 changes: 59 additions & 0 deletions src/cache/src/AnyModeTaggedCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
use BadMethodCallException;
use DateInterval;
use DateTimeInterface;
use Hypervel\Cache\Events\CacheHit;
use Hypervel\Cache\Events\CacheMissed;
use Hypervel\Cache\Events\ForgettingKey;
use Hypervel\Cache\Events\KeyForgetFailed;
use Hypervel\Cache\Events\KeyForgotten;
use Hypervel\Cache\Events\RetrievingKey;
use UnitEnum;

use function Hypervel\Support\enum_value;

/**
* Tagged cache for any-mode tag semantics.
*
Expand Down Expand Up @@ -118,4 +126,55 @@ public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int|n
. 'its TTL; a direct Cache::touch() uses the store\'s plain-key semantics.'
);
}

/**
* Retrieve a plain-key item without exposing reads through the any-mode API.
*/
protected function getPlainRaw(UnitEnum|string $key): mixed
{
$key = $key instanceof UnitEnum ? (string) enum_value($key) : $key;

$this->event(RetrievingKey::class, fn (): RetrievingKey => new RetrievingKey($this->getName(), $key));

$value = $this->handleIncompleteClass($key, $this->store->get($key));

if (is_null($value)) {
$this->event(CacheMissed::class, fn (): CacheMissed => new CacheMissed($this->getName(), $key));
} else {
$this->event(
CacheHit::class,
fn (): CacheHit => new CacheHit($this->getName(), $key, NullSentinel::unwrap($value))
);
}

return $value;
}

/**
* Retrieve a plain-key item for remember operations.
*/
protected function getRawForRemember(UnitEnum|string $key): mixed
{
return $this->getPlainRaw($key);
}

/**
* Remove a plain-key item without exposing deletes through the any-mode API.
*/
protected function forgetPlainKey(UnitEnum|string $key): bool
{
$key = $key instanceof UnitEnum ? (string) enum_value($key) : $key;

$this->event(ForgettingKey::class, fn (): ForgettingKey => new ForgettingKey($this->getName(), $key));

$result = $this->store->forget($key);

if ($result) {
$this->event(KeyForgotten::class, fn (): KeyForgotten => new KeyForgotten($this->getName(), $key));
} else {
$this->event(KeyForgetFailed::class, fn (): KeyForgetFailed => new KeyForgetFailed($this->getName(), $key));
}

return $result;
}
}
Loading