Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

In Memory Cache Laravel Package

beste/in-memory-cache

A PSR-6 in-memory cache for PHP that works as a sensible default implementation and in tests. It supports PSR-20 clocks, including frozen clocks, so you can test time-based expiry without a real cache server. Simple, fast, and dependency-free. Install with composer require beste/in-memory-cache.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements first: beste/in-memory-cache 1.5.x requires PHP 8.3+. On Laravel 10 (PHP 8.1/8.2) Composer will refuse to install it, so pin the older minor:

# PHP 8.3+ (Laravel 11/12)
composer require beste/in-memory-cache --dev

# PHP 8.1 / 8.2 (Laravel 10)
composer require "beste/in-memory-cache:1.4.*" --dev

Note the --dev. In a Laravel app this is almost always a test utility, not a production cache driver — see Gotchas for why.

Where to look first: there is only one class that matters, Beste\Cache\InMemoryCache, implementing Psr\Cache\CacheItemPoolInterface. Its constructor is __construct(?ClockInterface $clock = null). That's the entire public surface. Skim the README, then the changelog (1.2.0 relaxed key rules, 1.3.1 made the clock nullable) and you're done — there is no config file, no service provider, no facade.

First use case: a service in your app that type-hints PSR-6 rather than Laravel's Repository contract.

namespace App\Services;

use Psr\Cache\CacheItemPoolInterface;

final class ExchangeRateProvider
{
    public function __construct(private CacheItemPoolInterface $cache) {}

    public function rateFor(string $currency): float
    {
        $item = $this->cache->getItem('fx.'.$currency);

        if (! $item->isHit()) {
            $item->set($this->fetchFromApi($currency))->expiresAfter(300);
            $this->cache->save($item);
        }

        return $item->get();
    }
}

In the test, give it a real pool instead of a mock:

$provider = new ExchangeRateProvider(new InMemoryCache());

$this->assertSame(1.08, $provider->rateFor('EUR'));
$this->assertSame(1.08, $provider->rateFor('EUR')); // served from cache, no second HTTP call

That's the 60-second version: replace $this->createMock(CacheItemPoolInterface::class) with a working implementation you can assert against after the fact.

Implementation Patterns

1. Container binding for PSR-6-typed services

The cleanest day-to-day use is: your production code depends on Psr\Cache\CacheItemPoolInterface, Laravel binds a real pool in production, and your test suite swaps in the in-memory one.

// AppServiceProvider::register()
$this->app->singleton(CacheItemPoolInterface::class, function ($app) {
    return $app->make(SomeRealPsr6Pool::class); // e.g. Symfony Cache, Redis PSR-6 adapter
});
// tests/TestCase.php
protected function setUp(): void
{
    parent::setUp();

    $this->app->instance(CacheItemPoolInterface::class, new InMemoryCache());
}

Because it's singleton-scoped to the app instance, every test case gets a fresh, empty pool — no Redis::flushdb(), no flaky network.

2. Bridging into the Cache facade via Laravel's PSR-6 adapter

If the code under test goes through Cache::get() / Cache::remember() rather than a PSR-6 type-hint, wrap the pool with Illuminate\Cache\Psr6Cache (which extends Psr16Cache and implements Illuminate\Contracts\Cache\Repository):

// AppServiceProvider::boot()  — register only in non-production
use Beste\Cache\InMemoryCache;
use Illuminate\Cache\Psr6Cache;
use Illuminate\Support\Facades\Cache;

Cache::extend('in-memory', fn () => new Psr6Cache(new InMemoryCache()));
// config/cache.php
'in-memory' => ['driver' => 'in-memory'],
// phpunit.xml:  <env name="CACHE_STORE" value="in-memory"/>
Cache::store('in-memory')->remember('stats', 60, fn () => $this->collect());

This gives facade-level realism (remember, put, forget, flush) with zero I/O. Check that Illuminate\Cache\Psr6Cache exists in your Laravel version; if it doesn't, a 30-line decorator over the pool is trivial.

3. Deterministic TTL tests with a frozen clock

travel() / Carbon::setTestNow() will not move this cache — it reads a PSR-20 clock. Inject Beste\Clock\FrozenClock:

use Beste\Cache\InMemoryCache;
use Beste\Clock\FrozenClock;

$clock = FrozenClock::fromUTC();
$cache = new InMemoryCache($clock);

$item = $cache->getItem('token');
$item->set('abc')->expiresAfter(new DateInterval('PT1H'));
$cache->save($item);

$clock->setTo($clock->now()->add(new DateInterval('PT59M')));
$this->assertTrue($cache->getItem('token')->isHit());

$clock->setTo($clock->now()->add(new DateInterval('PT2M')));
$this->assertFalse($cache->getItem('token')->isHit());

This is the feature you can't easily fake with Cache::fake() and the main reason to reach for this package over a plain array.

4. Seed/assert pattern instead of mocks

Rather than asserting that save() was called, seed the pool and assert behaviour:

$cache = new InMemoryCache();
$cache->save($cache->getItem('exchange.eur')->set(1.08));

$sut = new PriceCalculator($cache);
$this->assertSame(10.80, $sut->priceInEur(10.0));

5. Long-running processes (Octane, queue workers)

Under PHP-FPM you get a fresh pool per request, which is usually what you want. Under Octane the pool survives between requests and tenants — bind it scoped() and clear it in terminate() if you use it at all there.

Gotchas and Tips

  • It is not a Laravel cache Store. Cache::fake(), Cache::spy(), and cache()->driver() know nothing about it. Everything routes through Psr6Cache or a PSR-6 type-hint.

  • The "production fallback cache" pitch doesn't hold for Laravel. Under PHP-FPM the pool is destroyed at the end of every request, so it caches nothing across requests — a fallback that only lives for the current request is a placebo, not resilience. Under Octane it persists, which means unbounded memory growth and cross-tenant leakage. Treat it as a test double and per-request scratch cache, and keep the real Redis/Memcached fallback story for a real store.

  • PSR-6 reserved characters will bite you. { } ( ) / \ @ : are forbidden in cache keys. Laravel-style keys such as user:42, api/rates/usd, or @handle throw InvalidArgumentException. Normalise before caching:

    $key = str_replace(['{','}','(',')','/', '\\', '@', ':'], '.', $key);
    

    (Dashes and >64-char keys are fine — that restriction was lifted in 1.2.0.)

  • The README clock example is buggy. It builds $clock and then calls new InMemoryCache() without passing it, so nothing is frozen. Pass new InMemoryCache($clock).

  • Objects are stored by reference, not serialized. Mutating an object after save() mutates the cached entry — unlike Redis. Never hold a reference and assume snapshot semantics.

  • It can hide serialization bugs. Because there's no serialize()/unserialize() boundary, closures, resources, or non-serializable objects will "work" in tests and blow up against Redis. Keep at least one integration test against the real store for anything non-scalar.

  • A miss is indistinguishable from a cached null without isHit(). Always check isHit(), never get() === null.

  • Grab items fresh after time travel. $item is bound to the moment it was fetched; re-call getItem() after advancing the clock rather than reusing the old item.

  • No eviction, no max size, no TTL sweep. Expired entries are only removed lazily. In queue workers or long Pest/PHPUnit runs, call $cache->clear() in tearDown(); there's no key-enumeration API, so track the keys you seed in a helper if you need to assert on pool contents.

  • Feature gaps through the Laravel adapter: tags, Cache::lock(), rate limiting, and WithoutOverlapping won't work — locks throw and increment/decrement are non-atomic read-modify-write. flush() maps to clear().

  • Pin 1.4.* on PHP < 8.3, or Composer will silently fail to resolve and you'll spend ten minutes blaming a different package.

  • Extension point: wrap the pool in a CacheItemPoolInterface decorator for hit/miss counters or call logs rather than subclassing; it keeps the boundary explicit and works identically against the real pool in production.

  • Risk profile is low but narrow: 26 stars, 0 dependents, same author as beste/clock, MIT. Tiny, stable, well-tested surface — but don't build product features on it, and don't expect community answers when something looks odd.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels