/CacheQuery

Remember your query results using only one method. Yes, only one.

Primary LanguagePHPMIT LicenseMIT

Cache Query

Latest Version on Packagist Latest stable test run Codecov coverage Maintainability Sonarcloud Status Laravel Octane Compatibility

Remember your query results using only one method. Yes, only one.

Articles::latest('published_at')->cache()->take(10)->get();

Keep this package free

Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can spread the word!

Requirements

  • PHP 8.0
  • Laravel 9.x

Installation

You can install the package via composer:

composer require laragear/cache-query

Usage

Just use the cache() method to remember the results of a query for a default of 60 seconds.

use Illuminate\Support\Facades\DB;
use App\Models\Article;

$database = DB::table('articles')->latest('published_at')->take(10)->cache()->get();

$eloquent = Article::latest('published_at')->take(10)->cache()->get();

The next time you call the same query, the result will be retrieved from the cache instead of running the SELECT SQL statement in the database, even if the results are empty, null or false.

Since it's eager load unaware, you can also cache (or not) an eager loaded relation.

use App\Models\User;

$eloquent = User::where('is_author')->with('posts' => function ($posts) {
    $post->cache()->where('published_at', '>', now());
})->paginate();

Time-to-live

By default, results of a query are cached by 60 seconds, but you're free to use any length, Datetime, DateInterval or Carbon instance.

use Illuminate\Support\Facades\DB;
use App\Models\Article;

DB::table('articles')->latest('published_at')->take(10)->cache(120)->get();

Article::latest('published_at')->take(10)->cache(now()->addHour())->get();

Custom Cache Key

The auto-generated cache key is an BASE64-MD5 hash of the SQL query and its bindings, which avoids any collision with other queries while keeping the cache key short for a faster lookup in the cache store.

Article::latest('published_at')->take(10)->cache(30, 'latest_articles')->get();

You can use this to your advantage to manually retrieve the result across your application:

$cachedArticles = Cache::get('cache-query|latest_articles');

Custom Cache Store

You can use any other Cache Store different from the application default by setting a third parameter, or a named parameter.

Article::latest('published_at')->take(10)->cache(store: 'redis')->get();

Cache Lock (data races)

On multiple processes, the query may be executed multiple times until the first process is able to store the result in the cache, specially when these take more than one second. To avoid this, set the wait parameter with the number of seconds to hold the acquired lock.

Article::latest('published_at')->take(200)->cache(wait: 5)->get();

The first process will acquire the lock for the given seconds and execute the query. The next processes will wait the same amount of seconds until the first process stores the result in the cache to retrieve it.

If you need a more advanced locking mechanism, use the cache lock directly.

Forgetting results

If you need to forget a result anywhere in your application, you should use a named key for your cached query, as autogenerated query keys are difficult (or plain impossible) to guess. Once you do, use the cache-query:forget command with the name of the key.

User::query()->cache(store: 'redis', key: 'find_joe')->whereName('Joe')->whereAge(20)->first();
php artisan cache-query:forget find_joe --store=redis

# Successfully removed [find_joe] from the [redis] cache store. 

Caveats

This cache package does some clever things to always retrieve the data from the cache, or populate it with the results, in an opaque way and using just one method, but this world is far from perfect.

Operations are NOT commutative

Altering the Builder methods order will change the auto-generated cache key. Even if two or more queries are visually the same, the order of statements makes the hash completely different.

For example, given two similar queries in different parts of the application, these both will not share the same cached result:

User::query()->cache()->whereName('Joe')->whereAge(20)->first();
// Cache key: "cache-query|/XreUO1yaZ4BzH2W6LtBSA=="

User::query()->cache()->whereAge(20)->whereName('Joe')->first();
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA=="

To ensure you're hitting the same cache on similar queries, use a custom cache key. With this, all queries using the same key will share the same cached result:

User::query()->cache(key: 'find_joe')->whereName('Joe')->whereAge(20)->first();
User::query()->cache(key: 'find_joe')->whereAge(20)->whereName('Joe')->first();

Eager load unaware

Since caching only works for the current query builder instance, an underlying Eager Load query won't be cached, as it's executed later in the pipeline. This may be a blessing or a curse depending on your scenario.

$page = 1;

User::with('posts', function ($posts) use ($page) {
    return $posts()->forPage($page);
})->cache()->find(1);

In the example, the posts eager load query results are never cached. To avoid that, you may use cache() on the eager loaded query. This way both the parent user query and the child posts query will be saved into the cache.

$page = 1;

User::with('posts', function ($posts) use ($page) {
    return $posts()->cache()->forPage($page);
})->find(1);

Alternatively, you can cache the whole results manually using remember():

$page = 1;

cache()->remember('cache_whole_results', function () use ($page) {
    User::with('posts', function ($posts) use ($page) {
        return $posts()->cache()->forPage($page);
    })->find(1);
})

Cannot delete autogenerated keys

When a key is not issued, the cache crates a hash based on the query, making it extremely difficult to remove it from the cache. If you need to remove the results from the cache at any given time, you should use a custom key and the included cache-query:forget command.

PhpStorm stubs

For users of PhpStorm, there is a stub file to aid in macro autocompletion for this package. You can publish them using the phpstorm tag:

php artisan vendor:publish --provider="Laragear\CacheQuery\CacheQueryServiceProvider" --tag="phpstorm"

The file gets published into the .stubs folder of your project. You should point your PhpStorm to these stubs.

How it works?

When you use cache(), it will wrap the connection into a CacheAwareProxy proxy, and proxy all method calls to it.

Once a SELECT statement is executed, it will check if the results are in the cache before executing the query. On Cache hit, it will return the cached results.

Laravel Octane compatibility

  • There are no singletons using a stale application instance.
  • There are no singletons using a stale config instance.
  • There are no singletons using a stale request instance.
  • There are no static properties written during a request.

There should be no problems using this package with Laravel Octane.

Security

If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker.

License

This specific package version is licensed under the terms of the MIT License, at time of publishing.

Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2022 Laravel LLC.