troccoli's avatar

Cache does not persist within Livewire component

I need to store the state of a user, but instead of using fields in the users table I have decided to use a persistent cache. The reason is that I will have other use cases to store states not all related to the users, so I needed a more generic solution.

So, I created the migrations for persistent_cache and persistent_cache_lock as copies for the migrations for the cache provided by Laravel.

Then I configures my persistent cache as follows in config/cache.php:

'persistent' => [
    'driver' => 'database',
    'connection' => env('DB_CACHE_CONNECTION'),
    'table' => 'persistent_cache',
    'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
    'lock_table' => 'persistent_cache_locks',
],

In my mind, with these settings I could just use the Cache facade as normal, but just specifying this store: for example Cache::store('persistent')->put($key, $value).

My Livewire component's method needs to fire an event when the user has filled in their profile. The method is

public function completeProfile(): void
{
    // Save user's profile
    // ...

    $cacheKey = sprintf(SignUpAttempt::CACHE_KEY_TEMPLATE, $user->getKey());
    $signUpAttempt = Cache::store('persistent')->get(key: $cacheKey);

    /** @var ?SignUpAttempt $signUpAttempt */
    if (filled($signUpAttempt)) {
        Cache::store('persistent')->forget(key: $cacheKey);
        event(new UserSignedUp($user, $signUpAttempt));
    }

    $this->redirectIntended(default: route('home'), navigate: true);
}

Now, I want to write a test, and I thought I could store a value in the cache, complete the profile, and then assert the event was fired with the correct details. So here's my test:

it('fires the UserSignedUp', function () {
    Event::fake();
    $user = User::factory()->withoutProfile()->create();

    $cacheKey = sprintf(SignUpAttempt::CACHE_KEY_TEMPLATE, $user->getKey());
    Cache::store('persistent')->put(key: $cacheKey, value: new SignUpAttempt(...));

    Livewire::actingAs($user)
        ->test(CompleteProfile::class)
        ->set('form.firstName', $firstName = fake()->firstName())
        ->set('form.lastName', $lastName = fake()->lastName())
        ->set('form.company', $company = fake()->company())
        ->set('form.jobTitle', $jobTitle = fake()->jobTitle())
        ->call('completeProfile')
        ->assertHasNoErrors();

    Event::assertDispatched(UserSignedUp::class);
});

The test fails as the event is never dispatched.

Now, the thing is, if I change the component and the test to use the DB facade and write into the persistent_cache table directly, then the test passes. But I really do not understand why. So does anybody have any ideas why the test fails if I use the Cache facade?

By the way, I'm using a real, as in not in memory, SQLite database, and the RefreshDatabase trait.

0 likes
3 replies
LaryAI's avatar
Level 58

The problem you're running into is related to the way Laravel handles cache during testing, especially when using the RefreshDatabase trait and custom cache stores that use the database driver.

Why the Cache Does Not Persist

  • Separate Database Connections: When your tests run, the default database connection is often set to a special SQLite testing database (often in-memory, though you say you're using a real SQLite DB file). The Cache store you've defined (with the 'persistent' key) will by default use the database and table you configure, but it may not be using the same connection/transaction as your main DB connection within tests.
  • Transactions & Caching: The RefreshDatabase trait wraps each test in a transaction and rolls it back at the end. The database cache driver does not participate in these transactions for cache tables—even if it's using the same connection—so cache writes in one process (e.g., your test setup) might not be visible in another (like the Livewire component's process).
  • Serialization/Persistence: Also, cache drivers serialize data. If you're caching an object like new SignUpAttempt(...), it must be serializable and autoloadable in both the test and component context. If there are differences or issues in serialization during testing, the object may not fully persist.

What Is Happening

  1. You write to the cache in your test with Cache::store('persistent')->put(...).
  2. The Livewire component (running in a separate HTTP request context, simulated by Livewire) tries to read from the same cache.
  3. Because of either connection separation, DB transaction/rollbacks, or object serialization, the component can’t read the cache entry.
  4. Hence, your event never fires.

Why Using DB Facade Directly Works

  • When using DB::table(...), you directly manipulate the table and sidestep cache driver logic and serialization, ensuring both the test and Livewire component can read/write as expected, assuming they're on the same connection.

Solution

1. Ensure the Cache is Using the Test Database Connection

Make sure your persistent cache store uses the same connection as your tests. In phpunit.xml, you probably set DB_CONNECTION=sqlite. You should make sure that both the main and cache connections use the same database file (or in-memory database, but beware of its isolation in separate processes).

Example .env.testing or phpunit.xml snippet:

DB_CONNECTION=sqlite
DB_DATABASE=/full/path/to/database/database.sqlite
DB_CACHE_CONNECTION=sqlite
DB_CACHE_DATABASE=/full/path/to/database/database.sqlite

And in config/cache.php:

'persistent' => [
    'driver' => 'database',
    'connection' => env('DB_CACHE_CONNECTION', 'sqlite'),
    'table' => 'persistent_cache',
    ...
],

Tip: Dump the database connection in both test and component to confirm they're the same.

2. Serialize Your Objects with Cache

If you cache an object (SignUpAttempt), make sure it and any dependencies are properly serializable, and ensure all related classes are included in the test process and Livewire context.

For testing, prefer using simple data structures (arrays) if possible.

3. Workaround for Tests: Use Cache Fake in Testing

One way to make your test simpler and avoid DB-based cache persisting issues is to use the Cache fake. You'll mock the cache methods for the duration of your test, ensuring the test and the component use the same in-memory cache.

Add this at the beginning of your test:

Cache::store('persistent')->flush();
Cache::store('persistent')->forget($cacheKey);

Or better: Use Laravel’s cache fake:

Cache::store('persistent')->flush();
Cache::shouldReceive('store')
    ->with('persistent')
    ->andReturnSelf();

But in general, by default, Laravel’s Cache::fake() only fakes the default cache store, so you can use this package or implement a custom fake for your named store. Otherwise, ensure both the test and the component are operating in the same context and on the same database.

4. Avoid Caching in Database in Test—Use Array Driver

For tests only, you could override the driver to use the in-memory array driver. In phpunit.xml or .env.testing:

CACHE_DRIVER=array

Or for your persistent store:

PERSISTENT_CACHE_DRIVER=array

And in your config/cache.php:

'persistent' => [
    'driver' => env('PERSISTENT_CACHE_DRIVER', 'array'),
    ...
],

This will avoid any database persistence and test isolation problems—at the small cost of not testing the real backing store, but it’s very reliable for most unit/integration tests.


Summary: How to Fix Your Test

  • Make sure your 'persistent' cache store is really using the same database and connection as the test.
  • For cache tests, prefer using the array cache (in-memory) for reliability, or use simple, serializable values (arrays) instead of objects in the cache.
  • Flush/forget cache keys at the start of each test to avoid state leakage.
  • Debug the raw contents of the persistent_cache table to confirm what’s being written/read.

Example: Recommended Test Setup

it('fires the UserSignedUp', function () {
    Event::fake();
    $user = User::factory()->withoutProfile()->create();

    $cacheKey = sprintf(SignUpAttempt::CACHE_KEY_TEMPLATE, $user->getKey());

    // Prefer array/simple structure for cache value
    Cache::store('persistent')->put($cacheKey, ['foo' => 'bar']);

    Livewire::actingAs($user)
        ->test(CompleteProfile::class)
        ->set('form.firstName', $firstName = fake()->firstName())
        ->set('form.lastName', $lastName = fake()->lastName())
        ->set('form.company', $company = fake()->company())
        ->set('form.jobTitle', $jobTitle = fake()->jobTitle())
        ->call('completeProfile')
        ->assertHasNoErrors();

    Event::assertDispatched(UserSignedUp::class);
});

If you still have issues, try logging the output of Cache::store('persistent')->get($cacheKey) just before and inside the Livewire component for debugging.


Further Reading

Let me know if you need a more concrete example based on your actual connection/environment setup!

DoubleClickDesignLtd's avatar

@troccoli I'm not 100% sure why your doing the persistent cache from what your describing.

However to answer your test fail issue, I believe this happens because Laravel’s test environment fakes the cache store, so the value you put() in the test is not available when the Livewire request runs.

Livewire component actions (->call('completeProfile')) are executed through a separate HTTP request lifecycle.

In your test, when you do the below, that value is stored in the cache instance for the current test lifecycle.

Cache::store('persistent')->put(key: $cacheKey, value: new SignUpAttempt(...));

During the Livewire call, Laravel boots a new request and the cache store is resolved again. In tests, Laravel commonly swaps cache stores with an array / fake store (or otherwise non-persistent store), so your “persistent” store isn’t actually hitting your persistent_cache DB table during the Livewire request.

That why the test is failing from my understand. To fix try forcing Laravel to use the real cache store in tests, not the fake/array one.

Question: Have you ensure cache tables exist in test DB and you set up the phpunit.xml to use your cache driver?

Let me know how you get on.

All the best

Mark

troccoli's avatar

Thank you for your answer.

The reason I looked into a cache is because I need to store the state of a user between requests.

As far I understand it's up to you to tell which default store to use during testing. I agree it's usually array but it could be anything else. In any case that is true for the default store.

In my code I don't use the default cache store, but the persistent one directly, which uses the database driver, and therefore store the data in the persistent_cache table.

As I said in my message, when use the DB facade in the code and test directly then the test passes. So, in the end I decided to sort of implement the cache myself. I created a table user_contexts and its Model, and store, retrieve, and delete the data as any other model.

As you can see from my code I didn't really need a cache, as that data does not expire. I just thought it was a nice use of it because a) I may in future have the need to expire the data (but KISS right?) and b) I had access to various methods and artisan command to clear the cache.

Anyway, I resolved the whole issue by adding a table to the DB and use it for this sort of data.

Please or to participate in this conversation.