Skip to content

Commit e08623d

Browse files
5.x
1 parent 8e0afa9 commit e08623d

13 files changed

+1113
-505
lines changed

.github/workflows/php.yml

+3-4
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,18 @@ jobs:
5050
strategy:
5151
matrix:
5252
php-version:
53-
- 8.1
5453
- 8.2
5554
- 8.3
5655
- 8.4
5756
laravel-constraint:
58-
- 10.*
5957
- 11.*
58+
- 12.*
6059
dependencies:
6160
- lowest
6261
- highest
6362
exclude:
64-
- laravel-constraint: 11.*
65-
php-version: 8.1
63+
- laravel-constraint: 12.*
64+
php-version: 8.2
6665
steps:
6766
- name: Set up PHP
6867
uses: shivammathur/setup-php@v2

.stubs/stubs

+10-20
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,41 @@ namespace Illuminate\Database\Query {
44

55
use DateInterval;
66
use DateTimeInterface;
7+
use Closure;
8+
use Laragear\CacheQuery\Cache;
79

810
class Builder
911
{
1012
/**
1113
* Caches the underlying query results.
1214
*
13-
* @param \DateTimeInterface|\DateInterval|int|bool|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|null $ttl
14-
* @param string $key
15-
* @param string|null $store
16-
* @param int $wait
15+
* @param \DateTimeInterface|\DateInterval|\Laragear\CacheQuery\Cache|(\Closure(\Laragear\CacheQuery\Cache):void)|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl
1716
* @return $this
1817
*/
19-
public function cache(
20-
DateTimeInterface|DateInterval|int|bool|array|null $ttl = null,
21-
string $key = '',
22-
string $store = null,
23-
int $wait = 0,
24-
): static {
18+
public function cache(DateTimeInterface|DateInterval|Closure|Cache|int|array|string|null $ttl = 60): static
19+
{
2520
//
2621
}
2722
}
2823
}
2924

3025
namespace Illuminate\Database\Eloquent {
3126

27+
use Closure;
3228
use DateInterval;
3329
use DateTimeInterface;
30+
use Laragear\CacheQuery\Cache;
3431

3532
class Builder
3633
{
3734
/**
3835
* Caches the underlying query results.
3936
*
40-
* @param \DateTimeInterface|\DateInterval|int|bool|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|null $ttl
41-
* @param string $key
42-
* @param string|null $store
43-
* @param int $wait
37+
* @param \DateTimeInterface|\DateInterval|\Laragear\CacheQuery\Cache|(\Closure(\Laragear\CacheQuery\Cache):void)|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl
4438
* @return $this
4539
*/
46-
public function cache(
47-
DateTimeInterface|DateInterval|int|bool|array|null $ttl = null,
48-
string $key = '',
49-
string $store = null,
50-
int $wait = 0,
51-
): static {
40+
public function cache(DateTimeInterface|DateInterval|Closure|Cache|int|array|string|null $ttl = 60): static
41+
{
5242
//
5343
}
5444
}

README.md

+107-63
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ Articles::latest('published_at')->cache()->take(10)->get();
1616

1717
[![](.github/assets/support.png)](https://github.com/sponsors/DarkGhostHunter)
1818

19-
Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can **[spread the word!](http://twitter.com/share?text=I%20am%20using%20this%20cool%20PHP%20package&url=https://github.com%2FLaragear%2FCacheQuery&hashtags=PHP,Laravel)**
19+
Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can **spread the word in social media**
2020

2121
## Requirements
2222

23-
* Laravel 10 or later
23+
* Laravel 11 or later
2424

2525
## Installation
2626

@@ -30,6 +30,12 @@ You can install the package via composer:
3030
composer require laragear/cache-query
3131
```
3232

33+
## How it works?
34+
35+
This library wraps the connection into a proxy object. It proxies all method calls to it except `select()` and `selectOne()`.
36+
37+
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.
38+
3339
## Usage
3440

3541
Just use the `cache()` method to remember the results of a query for a default of 60 seconds.
@@ -43,9 +49,9 @@ DB::table('articles')->latest('published_at')->take(10)->cache()->get();
4349
Article::latest('published_at')->take(10)->cache()->get();
4450
```
4551

46-
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`.
52+
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`. You may also desire to [not cache empty results](#cache-except-empty-results).
4753

48-
It's **eager load aware**. This means that it will cache an eager loaded relation automatically.
54+
It's **eager load aware**. This means that it will cache an eager loaded relation automatically, but [you may also disable this](#eager-loaded-queries).
4955

5056
```php
5157
use App\Models\User;
@@ -57,7 +63,7 @@ $usersWithPosts = User::where('is_author')->with('posts')->cache()->paginate();
5763

5864
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.
5965

60-
You're free to use any number of seconds from now, or just a Carbon instance.
66+
You're free to use any number of seconds from now, or a `DateTimeInterface` like Carbon.
6167

6268
```php
6369
use Illuminate\Support\Facades\DB;
@@ -68,79 +74,118 @@ DB::table('articles')->latest('published_at')->take(10)->cache(120)->get();
6874
Article::latest('published_at')->take(10)->cache(now()->addHour())->get();
6975
```
7076

71-
You can also use `null` to set the query results forever.
77+
You can also use `null`, `ever` or `forever` to set the query results forever.
7278

7379
```php
7480
use App\Models\Article;
7581

76-
Article::latest('published_at')->take(10)->cache(null)->get();
82+
Article::latest('published_at')->take(10)->cache('forever')->get();
7783
```
7884

79-
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.
85+
### Stale while revalidate
86+
87+
You may take advantage of [Laravel Flexible Caching mechanism](https://laravel.com/docs/cache#swr) by issuing an array of values as first argument. (...) _The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before recalculation is necessary_.
8088

8189
```php
8290
use App\Models\Article;
8391

84-
$regen = request()->isNotFilled('no-cache');
85-
86-
Article::latest('published_at')->take(10)->cache($regen)->get();
92+
Article::latest('published_at')->take(200)->cache([300, 60])->get();
8793
```
8894

89-
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.
95+
The above example will refresh the query results if there is 60 seconds lefts until the data dies.
96+
97+
## Advanced caching
98+
99+
You may use a callback to further change the query caching. The callback receives a `Laragear\CacheQuery\Cache` instance that allows to change how to cache the data.
90100

91101
```php
92-
use App\Models\Article;
102+
use Laragear\CacheQuery\Cache;
103+
use App\Models\User;
93104

94-
Article::latest('published_at')->whereBelongsTo($user)->take(10)->unless(Auth::check(), function ($articles) {
95-
// If the user is a guest, use the cache to show the latest articles of the given user.
96-
$articles->cache();
105+
User::query()->where('cool', true)->cache(function (Cache $cache) {
106+
$cache->ttl([300, 60])->regenWhen(true);
97107
})->get();
98108
```
99109

100-
### Custom Cache Store
110+
Alternatively, you can create and configure an instance outside the query, and then pass it as an argument. You can do this with the `for()` method or `flexible()` method
101111

102-
You can use any other Cache Store different from the application default by setting a third parameter, or a named parameter.
112+
```php
113+
use Laragear\CacheQuery\Cache;
114+
use App\Models\User;
115+
use App\Models\Post;
116+
117+
$cacheUser = Cache::for(30)->regenWhen(true);
118+
119+
User::query()->where('cool', true)->cache($cacheUser)->get();
120+
121+
$cachePost = Cache::flexible(300, 50)->as('frontend-posts');
122+
123+
Post::query()->latest()->limit(10)->cache($cachePost)->get();
124+
```
125+
126+
### Conditional Regeneration
127+
128+
You may want to forcefully regenerate the queried cache when the underlying data changes, or because other reason. For that, use the `regenWhen()` and a condition that evaluates to `true`, and `regenUnless()` for a condition that evaluates to `false`. If you pass a callback, it will be executed before retrieving the results from the cache.
103129

104130
```php
105-
use App\Models\Article;
131+
use Laragear\CacheQuery\Cache;
132+
133+
Cache::for([300, 50])->regenWhen(true);
106134

107-
Article::latest('published_at')->take(10)->cache(store: 'redis')->get();
135+
Cache::for(50)->regenUnless(fn() => false);
108136
```
109137

110-
### Cache Lock (data races)
138+
### Cache except empty results
139+
140+
By default, the `cache()` method will cache _any_ result from the query, empty or not. You can disable this with the `exceptEmpty()` method, which will only cache non-empty results.
141+
142+
```php
143+
use Laragear\CacheQuery\Cache;
144+
145+
Cache::for(300)->exceptEmpty();
146+
```
111147

112-
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 1,000 times.
148+
### Eager loaded queries
113149

114-
To avoid this, set the `wait` parameter with the number of seconds to hold the acquired lock.
150+
You may disable caching Eager Loaded Queries with the `exceptNested()` method. With that, only the query that invokes the `cache()` method will be cached.
115151

116152
```php
117-
use App\Models\Article;
153+
use Laragear\CacheQuery\Cache;
118154

119-
Article::latest('published_at')->take(200)->cache(wait: 5)->get();
155+
Cache::for(300)->exceptNested();
120156
```
121157

122-
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.
158+
For example, in this query, only the `User` query will be cached, while the `posts` won't.
123159

124-
> If you need a more advanced locking mechanism, use the [cache lock](https://laravel.com/docs/cache#managing-locks-across-processes) directly.
160+
```php
161+
use App\Models\User;
162+
use App\Models\Post;
163+
use Laragear\CacheQuery\Cache;
125164

126-
### Stale while revalidate
165+
User::where('cool', true)
166+
->cache(fn(Cache $cache) => $cache->exceptNested())
167+
->with('posts', fn ($query) => $query->where('published_at', '<', now())
168+
->get();
169+
```
170+
171+
### Custom Store
127172

128-
You may take advantage of [Laravel Flexible Caching mechanism](https://laravel.com/docs/11.x/cache#swr) by issuing an array of values as first argument. (...) _The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before recalculation is necessary_.
173+
By default, the cached results use your application default cache store. You may change the default store using the `store()` method.
129174

130175
```php
131-
use App\Models\Article;
176+
use Laragear\CacheQuery\Cache;
132177

133-
Article::latest('published_at')->take(200)->cache([5, 300])->get();
178+
Cache::for(300)->store('redis');
134179
```
135180

136-
## Forgetting results with a key
181+
### Forgetting cached results
137182

138-
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.
183+
If you plan to remove a query from the cache, you will need to identify the query with the `as()` method and an identifiable key name.
139184

140185
```php
141-
use App\Models\Article;
186+
use Laragear\CacheQuery\Cache;
142187

143-
Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get();
188+
Cache::for(300)->as('latest_articles');
144189
```
145190

146191
Once done, you can later delete the query results using the `CacheQuery` facade.
@@ -165,13 +210,18 @@ You may use the same key for multiple queries to group them into a single list y
165210
use App\Models\Article;
166211
use App\Models\Post;
167212
use Laragear\CacheQuery\Facades\CacheQuery;
213+
use Laragear\CacheQuery\Cache;
168214

169-
Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get();
170-
Post::latest('posted_at')->take(10)->cache(key: 'latest_articles')->get();
215+
$cache = Cache::for(300)->as('latest_articles');
216+
217+
Article::latest('published_at')->with('drafts')->take(5)->cache($cache)->get();
218+
Post::latest('posted_at')->take(10)->cache($cache)->get();
171219

172220
CacheQuery::forget('latest_articles');
173221
```
174222

223+
> [!TIP]
224+
>
175225
> This functionality does not use cache tags, so it will work on any cache store you set, even the `file` driver!
176226
177227
## Custom Hash Function
@@ -184,13 +234,13 @@ This can be done in the `register()` method of your `AppServiceProvider`.
184234
namespace App\Providers;
185235

186236
use Illuminate\Support\ServiceProvider;
187-
use Laragear\CacheQuery\CacheAwareConnectionProxy;
237+
use Laragear\CacheQuery\Proxy;
188238

189239
class AppServiceProvider extends ServiceProvider
190240
{
191241
public function register()
192242
{
193-
CacheAwareConnectionProxy::$queryHasher = function ($connection, $query, $bindings) {
243+
Proxy::$queryHasher = function ($connection, $query, $bindings) {
194244
// ...
195245
}
196246
}
@@ -213,6 +263,7 @@ You will receive the `config/cache-query.php` config file with the following con
213263
return [
214264
'store' => env('CACHE_QUERY_STORE'),
215265
'prefix' => 'cache-query',
266+
'commutative' => false
216267
];
217268
```
218269

@@ -242,38 +293,37 @@ return [
242293

243294
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.
244295

245-
## Caveats
246-
247-
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.
248-
249-
### Operations are **NOT** commutative
296+
### Commutative operations
250297

251-
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.
298+
```php
299+
return [
300+
'commutative' => false
301+
]
302+
```
252303

253-
For example, given two similar queries in different parts of the application, these both will **not** share the same cached result:
304+
When _hashing_ queries, the default [hasher function](#custom-hash-function) will create different hashes even on visually different queries.
254305

255306
```php
256307
User::query()->cache()->whereName('Joe')->whereAge(20)->first();
257-
// Cache key: "cache-query|/XreUO1yaZ4BzH2W6LtBSA=="
308+
// Cache key: "cache-query|/XreUO1yaZ4BzH2W6LtBSA"
258309

259310
User::query()->cache()->whereAge(20)->whereName('Joe')->first();
260-
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA=="
311+
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA"
261312
```
262313

263-
To avoid this, ensure you always execute the same query, or centralize the query somewhere in your application (like using a [query scope](https://laravel.com/docs/11.x/eloquent#query-scopes)).
264-
265-
> **Note** This is by design. Ordering the query bindings would make operations commutative, but also disrupt [query-index optimizations](https://use-the-index-luke.com/sql/where-clause/the-equals-operator/concatenated-keys). Consider this not a bug, but a _feature_.
266-
267-
### Cannot delete autogenerated keys
268-
269-
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.
314+
By setting `commutative` to `true`, the function will always sort the query elements so similar queries share the same hash.
270315

271316
```php
317+
User::query()->cache()->whereName('Joe')->whereAge(20)->first();
318+
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA"
319+
272320
User::query()->cache()->whereAge(20)->whereName('Joe')->first();
273-
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA=="
321+
// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA"
274322
```
275323

276-
This makes extremely difficult to remove keys from the cache. If you need to invalidate or regenerate the cached results, [use a custom key](#forgetting-results-with-a-key).
324+
> [!TIP]
325+
>
326+
> This can be also overridden using your own [custom hash function](#custom-hash-function).
277327
278328
## PhpStorm stubs
279329

@@ -285,12 +335,6 @@ php artisan vendor:publish --provider="Laragear\CacheQuery\CacheQueryServiceProv
285335

286336
The file gets published into the `.stubs` folder of your project. You should point your [PhpStorm to these stubs](https://www.jetbrains.com/help/phpstorm/php.html#advanced-settings-area).
287337

288-
## How it works?
289-
290-
When you use `cache()`, it will wrap the connection into a proxy object. It proxies all method calls to it except `select()` and `selectOne()`.
291-
292-
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.
293-
294338
## Laravel Octane compatibility
295339

296340
- There are no singletons using a stale application instance.

0 commit comments

Comments
 (0)