laragear/cache-query

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

Fund package maintenance!
Github Sponsorship
Paypal

Installs: 45 021

Dependents: 0

Suggesters: 0

Security: 0

Stars: 255

Watchers: 1

Forks: 16

v4.1.0 2024-12-15 12:07 UTC

README

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();

Become a sponsor

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

Requirements

  • Laravel 10 or later

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;

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

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.

It's eager load aware. This means that it will cache an eager loaded relation automatically.

use App\Models\User;

$usersWithPosts = User::where('is_author')->with('posts')->cache()->paginate();

Time-to-live

By default, results of a query are cached by 60 seconds, which is mostly enough when your application is getting hammered with the same query results.

You're free to use any number of seconds from now, or just a 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();

You can also use null to set the query results forever.

use App\Models\Article;

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

Sometimes you may want to regenerate the results programmatically. To do that, set the time as false. This will repopulate the cache with the new results, even if these were not cached before.

use App\Models\Article;

$regen = request()->isNotFilled('no-cache');

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

Finally, you can bypass the cache entirely using the query builder when() and unless() methods easily, as these are totally compatible with the cache() method.

use App\Models\Article;

Article::latest('published_at')->whereBelongsTo($user)->take(10)->unless(Auth::check(), function ($articles) {
    // If the user is a guest, use the cache to show the latest articles of the given user.
    $articles->cache();
})->get();

Custom Cache Store

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

use App\Models\Article;

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. Take, for example, 1,000 users reading the latest 10 post of a site at the same time will call the database 100 times.

To avoid this, set the wait parameter with the number of seconds to hold the acquired lock.

use App\Models\Article;

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 the first process takes too much, the second will try again.

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

Forgetting results with a key

Cache keys are used to identify multiple queries cached with an identifiable name. These are not mandatory, but if you expect to remove a query from the cache, you will need to identify the query with the key argument.

use App\Models\Article;

Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get();

Once done, you can later delete the query results using the CacheQuery facade.

use Laragear\CacheQuery\Facades\CacheQuery;

CacheQuery::forget('latest_articles');

Or you may use the cache-query:forget command with the name of the key from the CLI.

php artisan cache-query:forget latest_articles

# Successfully removed [latest_articles] from the [file] cache store. 

You may use the same key for multiple queries to group them into a single list you can later delete in one go.

use App\Models\Article;
use App\Models\Post;
use Laragear\CacheQuery\Facades\CacheQuery;

Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get();
Post::latest('posted_at')->take(10)->cache(key: 'latest_articles')->get();

CacheQuery::forget('latest_articles');

This functionality does not use cache tags, so it will work on any cache store you set, even the file driver!

Custom Hash Function

You can set your own function to hash the incoming SQL Query. Just register your function in the $queryHasher static property of the CacheAwareConnectionProxy class. The function should receive the database Connection, the query string, and the SQL bindings in form of an array.

This can be done in the register() method of your AppServiceProvider.

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laragear\CacheQuery\CacheAwareConnectionProxy;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        CacheAwareConnectionProxy::$queryHasher = function ($connection, $query, $bindings) {
            // ...
        }
    }
}

Configuration

To further configure the package, publish the configuration file:

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

You will receive the config/cache-query.php config file with the following contents:

<?php

return [
    'store' => env('CACHE_QUERY_STORE'),
    'prefix' => 'cache-query',
];

Cache Store

return  [
    'store' => env('CACHE_QUERY_STORE'),
];

The default cache store to put the queried results. When not issued in the query, this setting will be used. If it's empty or null, the default cache store of your application will be used.

You can easily change this setting using your .env file:

CACHE_QUERY_STORE=redis

Prefix

return  [
    'prefix' => 'cache-query',
];

When storing query hashes and query named keys, this prefix will be appended, which will avoid conflicts with other cached keys. You can change in case it collides with other keys.

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 avoid this, ensure you always execute the same query, or centralize the query somewhere in your application (like using a query scope).

Note This is by design. Ordering the query bindings would make operations commutative, but also disrupt query-index optimizations. Consider this not a bug, but a feature.

Cannot delete autogenerated keys

All queries are cached using a BASE64 encoded MD5 hash of the connection name, SQL query and its bindings. This avoids any collision with other queries even from different databases, and also makes the cache lookup faster thanks to a shorter cache key.

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

This makes extremely difficult to remove keys from the cache. If you need to invalidate or regenerate the cached results, use a custom key.

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 proxy object. It proxies all method calls to it except select() and selectOne().

Once a SELECT statement is executed through the aforementioned methods, it will check if the results are in the cache before executing the query. On cache hit, it will return the cached results, otherwise it will continue execution, save the results using the cache configuration, and return them.

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.

Upgrading

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-2024 Laravel LLC.