From e08623db19d36f73744f6205c67db25a42c5fc97 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:34:13 -0300 Subject: [PATCH 01/10] 5.x --- .github/workflows/php.yml | 7 +- .stubs/stubs | 30 +- README.md | 170 ++++++---- UPGRADE.md | 16 +- composer.json | 16 +- config/cache-query.php | 13 + src/Cache.php | 230 +++++++++++++ src/CacheAwareConnectionProxy.php | 280 --------------- src/CacheQueryServiceProvider.php | 73 ++-- src/Proxy.php | 249 ++++++++++++++ src/Scopes/CacheRelations.php | 18 +- tests/CacheTest.php | 197 +++++++++++ ...eConnectionProxyTest.php => ProxyTest.php} | 319 +++++++++++++----- 13 files changed, 1113 insertions(+), 505 deletions(-) create mode 100644 src/Cache.php delete mode 100644 src/CacheAwareConnectionProxy.php create mode 100644 src/Proxy.php create mode 100644 tests/CacheTest.php rename tests/{CacheAwareConnectionProxyTest.php => ProxyTest.php} (72%) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ddc4529..69880f5 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -50,19 +50,18 @@ jobs: strategy: matrix: php-version: - - 8.1 - 8.2 - 8.3 - 8.4 laravel-constraint: - - 10.* - 11.* + - 12.* dependencies: - lowest - highest exclude: - - laravel-constraint: 11.* - php-version: 8.1 + - laravel-constraint: 12.* + php-version: 8.2 steps: - name: Set up PHP uses: shivammathur/setup-php@v2 diff --git a/.stubs/stubs b/.stubs/stubs index 5b7597d..7be21f1 100644 --- a/.stubs/stubs +++ b/.stubs/stubs @@ -4,24 +4,19 @@ namespace Illuminate\Database\Query { use DateInterval; use DateTimeInterface; + use Closure; + use Laragear\CacheQuery\Cache; class Builder { /** * Caches the underlying query results. * - * @param \DateTimeInterface|\DateInterval|int|bool|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|null $ttl - * @param string $key - * @param string|null $store - * @param int $wait + * @param \DateTimeInterface|\DateInterval|\Laragear\CacheQuery\Cache|(\Closure(\Laragear\CacheQuery\Cache):void)|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl * @return $this */ - public function cache( - DateTimeInterface|DateInterval|int|bool|array|null $ttl = null, - string $key = '', - string $store = null, - int $wait = 0, - ): static { + public function cache(DateTimeInterface|DateInterval|Closure|Cache|int|array|string|null $ttl = 60): static + { // } } @@ -29,26 +24,21 @@ namespace Illuminate\Database\Query { namespace Illuminate\Database\Eloquent { + use Closure; use DateInterval; use DateTimeInterface; + use Laragear\CacheQuery\Cache; class Builder { /** * Caches the underlying query results. * - * @param \DateTimeInterface|\DateInterval|int|bool|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|null $ttl - * @param string $key - * @param string|null $store - * @param int $wait + * @param \DateTimeInterface|\DateInterval|\Laragear\CacheQuery\Cache|(\Closure(\Laragear\CacheQuery\Cache):void)|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl * @return $this */ - public function cache( - DateTimeInterface|DateInterval|int|bool|array|null $ttl = null, - string $key = '', - string $store = null, - int $wait = 0, - ): static { + public function cache(DateTimeInterface|DateInterval|Closure|Cache|int|array|string|null $ttl = 60): static + { // } } diff --git a/README.md b/README.md index 5276682..4893ac8 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ Articles::latest('published_at')->cache()->take(10)->get(); [![](.github/assets/support.png)](https://github.com/sponsors/DarkGhostHunter) -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)** +Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can **spread the word in social media** ## Requirements -* Laravel 10 or later +* Laravel 11 or later ## Installation @@ -30,6 +30,12 @@ You can install the package via composer: composer require laragear/cache-query ``` +## How it works? + +This library wraps 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. + ## Usage 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(); 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`. +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). -It's **eager load aware**. This means that it will cache an eager loaded relation automatically. +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). ```php use App\Models\User; @@ -57,7 +63,7 @@ $usersWithPosts = User::where('is_author')->with('posts')->cache()->paginate(); 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. +You're free to use any number of seconds from now, or a `DateTimeInterface` like Carbon. ```php use Illuminate\Support\Facades\DB; @@ -68,79 +74,118 @@ 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. +You can also use `null`, `ever` or `forever` to set the query results forever. ```php use App\Models\Article; -Article::latest('published_at')->take(10)->cache(null)->get(); +Article::latest('published_at')->take(10)->cache('forever')->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. +### Stale while revalidate + +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_. ```php use App\Models\Article; -$regen = request()->isNotFilled('no-cache'); - -Article::latest('published_at')->take(10)->cache($regen)->get(); +Article::latest('published_at')->take(200)->cache([300, 60])->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. +The above example will refresh the query results if there is 60 seconds lefts until the data dies. + +## Advanced caching + +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. ```php -use App\Models\Article; +use Laragear\CacheQuery\Cache; +use App\Models\User; -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(); +User::query()->where('cool', true)->cache(function (Cache $cache) { + $cache->ttl([300, 60])->regenWhen(true); })->get(); ``` -### Custom Cache Store +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 -You can use any other Cache Store different from the application default by setting a third parameter, or a named parameter. +```php +use Laragear\CacheQuery\Cache; +use App\Models\User; +use App\Models\Post; + +$cacheUser = Cache::for(30)->regenWhen(true); + +User::query()->where('cool', true)->cache($cacheUser)->get(); + +$cachePost = Cache::flexible(300, 50)->as('frontend-posts'); + +Post::query()->latest()->limit(10)->cache($cachePost)->get(); +``` + +### Conditional Regeneration + +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. ```php -use App\Models\Article; +use Laragear\CacheQuery\Cache; + +Cache::for([300, 50])->regenWhen(true); -Article::latest('published_at')->take(10)->cache(store: 'redis')->get(); +Cache::for(50)->regenUnless(fn() => false); ``` -### Cache Lock (data races) +### Cache except empty results + +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. + +```php +use Laragear\CacheQuery\Cache; + +Cache::for(300)->exceptEmpty(); +``` -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. +### Eager loaded queries -To avoid this, set the `wait` parameter with the number of seconds to hold the acquired lock. +You may disable caching Eager Loaded Queries with the `exceptNested()` method. With that, only the query that invokes the `cache()` method will be cached. ```php -use App\Models\Article; +use Laragear\CacheQuery\Cache; -Article::latest('published_at')->take(200)->cache(wait: 5)->get(); +Cache::for(300)->exceptNested(); ``` -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. +For example, in this query, only the `User` query will be cached, while the `posts` won't. -> If you need a more advanced locking mechanism, use the [cache lock](https://laravel.com/docs/cache#managing-locks-across-processes) directly. +```php +use App\Models\User; +use App\Models\Post; +use Laragear\CacheQuery\Cache; -### Stale while revalidate +User::where('cool', true) + ->cache(fn(Cache $cache) => $cache->exceptNested()) + ->with('posts', fn ($query) => $query->where('published_at', '<', now()) + ->get(); +``` + +### Custom Store -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_. +By default, the cached results use your application default cache store. You may change the default store using the `store()` method. ```php -use App\Models\Article; +use Laragear\CacheQuery\Cache; -Article::latest('published_at')->take(200)->cache([5, 300])->get(); +Cache::for(300)->store('redis'); ``` -## Forgetting results with a key +### Forgetting cached results -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. +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. ```php -use App\Models\Article; +use Laragear\CacheQuery\Cache; -Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get(); +Cache::for(300)->as('latest_articles'); ``` 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 use App\Models\Article; use App\Models\Post; use Laragear\CacheQuery\Facades\CacheQuery; +use Laragear\CacheQuery\Cache; -Article::latest('published_at')->with('drafts')->take(5)->cache(key: 'latest_articles')->get(); -Post::latest('posted_at')->take(10)->cache(key: 'latest_articles')->get(); +$cache = Cache::for(300)->as('latest_articles'); + +Article::latest('published_at')->with('drafts')->take(5)->cache($cache)->get(); +Post::latest('posted_at')->take(10)->cache($cache)->get(); CacheQuery::forget('latest_articles'); ``` +> [!TIP] +> > This functionality does not use cache tags, so it will work on any cache store you set, even the `file` driver! ## Custom Hash Function @@ -184,13 +234,13 @@ This can be done in the `register()` method of your `AppServiceProvider`. namespace App\Providers; use Illuminate\Support\ServiceProvider; -use Laragear\CacheQuery\CacheAwareConnectionProxy; +use Laragear\CacheQuery\Proxy; class AppServiceProvider extends ServiceProvider { public function register() { - CacheAwareConnectionProxy::$queryHasher = function ($connection, $query, $bindings) { + Proxy::$queryHasher = function ($connection, $query, $bindings) { // ... } } @@ -213,6 +263,7 @@ You will receive the `config/cache-query.php` config file with the following con return [ 'store' => env('CACHE_QUERY_STORE'), 'prefix' => 'cache-query', + 'commutative' => false ]; ``` @@ -242,38 +293,37 @@ return [ 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 +### Commutative operations -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. +```php +return [ + 'commutative' => false +] +``` -For example, given two similar queries in different parts of the application, these both will **not** share the same cached result: +When _hashing_ queries, the default [hasher function](#custom-hash-function) will create different hashes even on visually different queries. ```php User::query()->cache()->whereName('Joe')->whereAge(20)->first(); -// Cache key: "cache-query|/XreUO1yaZ4BzH2W6LtBSA==" +// Cache key: "cache-query|/XreUO1yaZ4BzH2W6LtBSA" User::query()->cache()->whereAge(20)->whereName('Joe')->first(); -// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA==" +// 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](https://laravel.com/docs/11.x/eloquent#query-scopes)). - -> **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_. - -### 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. +By setting `commutative` to `true`, the function will always sort the query elements so similar queries share the same hash. ```php +User::query()->cache()->whereName('Joe')->whereAge(20)->first(); +// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA" + User::query()->cache()->whereAge(20)->whereName('Joe')->first(); -// Cache key: "cache-query|muDJevbVppCsTFcdeZBxsA==" +// 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](#forgetting-results-with-a-key). +> [!TIP] +> +> This can be also overridden using your own [custom hash function](#custom-hash-function). ## PhpStorm stubs @@ -285,12 +335,6 @@ php artisan vendor:publish --provider="Laragear\CacheQuery\CacheQueryServiceProv 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). -## 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. diff --git a/UPGRADE.md b/UPGRADE.md index 3bfcde9..e41e81f 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,6 +1,20 @@ # Upgrading -## From 2.x +## From 4.x + +### Regeneration + +Regeneration has been moved into the `Laragear\CacheQuery\Cache` class. If you need to programmatically regenerate results, use the `regenWhen()` and `regenUnless()` methods of the aforementioned class. + +### Locks + +Locks have been removed in favour of flexible caching, which under the hood already uses locks. If you're using locks, you should migrate to flexible caching. + +### Cache Store + +Custom Cache Store has been moved into the `Laragear\CacheQuery\Cache` class. If you need to use a non-default cache store, use the `store()` method of the aforementioned class. + +## From 2.x or 3.x ### Cache keys diff --git a/composer.json b/composer.json index 8cfd22a..262f4ec 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,16 @@ "issues": "https://github.com/laragear/cache-query/issues" }, "require": { - "php": "^8.1", - "illuminate/cache": "10.*|11.*", - "illuminate/config": "10.*|11.*", - "illuminate/database": "10.*|11.*", - "illuminate/support": "10.*|11.*", - "illuminate/container": "10.*|11.*", - "illuminate/contracts": "10.*|11.*" + "php": "^8.2", + "illuminate/cache": "11.*|10.*", + "illuminate/config": "11.*|10.*", + "illuminate/database": "11.*|10.*", + "illuminate/support": "11.*|10.*", + "illuminate/container": "11.*|10.*", + "illuminate/contracts": "11.*|10.*" }, "require-dev": { - "orchestra/testbench": "8.*|9.*" + "orchestra/testbench": "9.*|10.*" }, "autoload": { "psr-4": { diff --git a/config/cache-query.php b/config/cache-query.php index 74f4ba4..9dd94cc 100644 --- a/config/cache-query.php +++ b/config/cache-query.php @@ -28,4 +28,17 @@ 'prefix' => 'cache-query', + + /* + |-------------------------------------------------------------------------- + | Commutative + |-------------------------------------------------------------------------- + | + | Cached queries are identified by a key hash created by sorting the query + | data. This makes "visually" similar queries share the same cache key. + | If you don't want that, you can turn off commutative queries here. + | + */ + + 'commutative' => false, ]; diff --git a/src/Cache.php b/src/Cache.php new file mode 100644 index 0000000..e22746c --- /dev/null +++ b/src/Cache.php @@ -0,0 +1,230 @@ +ttl($ttl); + } + + /** + * Sets the custom store to use to save the query results. + * + * @return $this + */ + public function store(?string $store): static + { + $this->store = $store; + + return $this; + } + + /** + * Only save the cache results if these are not empty or null. + * + * @return $this + */ + public function exceptEmpty(): static + { + $this->saveEmptyResults = false; + + return $this; + } + + /** + * Only save the cache results for the query that invokes "cache()". + * + * @return $this + */ + public function exceptNested(): static + { + $this->saveNestedQueries = false; + + return $this; + } + + /** + * Regenerate the results of the query if the condition is truthy. + * + * @param (\Closure(\Illuminate\Database\Eloquent\Builder):mixed)|mixed $condition + * @return $this + */ + public function regenWhen(mixed $condition): static + { + $this->regenerate = $condition; + + return $this; + } + + /** + * Regenerate the results of the query if the condition is truthy. + * + * @param (\Closure(\Illuminate\Database\Eloquent\Builder):mixed)|mixed $condition + * @return $this + */ + public function regenIf(mixed $condition): static + { + return $this->regenWhen($condition); + } + + /** + * Regenerate the results of the query if the condition is falsy. + * + * @param (\Closure(\Illuminate\Database\Eloquent\Builder):mixed)|mixed $condition + * @return $this + */ + public function regenUnless(mixed $condition): static + { + $this->regenFactor = false; + + return $this->regenWhen($condition); + } + + /** + * Sets the key for the results of this query, so these can be forgotten later. + * + * @return $this + */ + public function as(string $key): static + { + $this->key = $key; + + return $this; + } + + /** + * Stores the cached results forever. + * + * @return $this + */ + public function ever(): static + { + $this->ttl = null; + + return $this; + } + + /** + * Stores the cached results until a given amount of seconds or datetime. + * + * @param \DateTimeInterface|\DateInterval|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl + * @return $this + */ + public function ttl(DateTimeInterface|DateInterval|int|array|null|string $ttl): static + { + if (is_string($ttl)) { + $ttl = in_array(Str::lower($ttl), ['ever', 'forever', 'null'], true) + ? null + : throw new InvalidArgumentException('The $ttl argument can only be "ever" or "forever" or "null".'); + } + + if (is_numeric($ttl)) { + $ttl = (int) $ttl; + } + + $this->ttl = $ttl; + + return $this; + } + + /** + * Stores the cached results until a given amount of seconds or datetime. + * + * @param \DateTimeInterface|\DateInterval|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl + * @return $this + */ + public function until(DateTimeInterface|DateInterval|int|array|null|string $ttl): static + { + return $this->ttl($ttl); + } + + /** + * Create a new Cache instance. + * + * @param \DateTimeInterface|\DateInterval|int|array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int }|string|null $ttl + */ + public static function for(DateTimeInterface|DateInterval|int|array|null|string $ttl): static + { + return new static($ttl); + } + + /** + * Regenerates the cached results before a specific amount of seconds before the data dies. + * + * @param array{ seconds?: int, owner?: string }|null $lock + * @return $this + */ + public static function flexible(int $seconds, int $stale, ?array $lock = null): static + { + $instance = new static([$seconds, $stale]); + $instance->lock = $lock; + + return $instance; + } +} diff --git a/src/CacheAwareConnectionProxy.php b/src/CacheAwareConnectionProxy.php deleted file mode 100644 index a3b167c..0000000 --- a/src/CacheAwareConnectionProxy.php +++ /dev/null @@ -1,280 +0,0 @@ -userKey) { - $this->userKey = $this->cachePrefix.'|'.$this->userKey; - } - } - - /** - * Run a select statement against the database. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @return mixed - */ - public function select($query, $bindings = [], $useReadPdo = true) - { - // Create the unique hash for the query to avoid any duplicate query. - $this->computedKey = $this->getQueryHash($query, $bindings); - - // We will append the previous related query to the computed key. - if ($this->queryKeySuffix) { - $this->computedKey = $this->queryKeySuffix.'.'.$this->computedKey; - } - - // We will use the prefix to operate on the cache directly. - $key = $this->cachePrefix.'|'.$this->computedKey; - - // If the user is setting an array, we will steer return the results using "flexible". - if (is_array($this->ttl) && count($this->ttl) > 1 && method_exists($this->repository, 'flexible')) { - return $this->returnResultsUsingFlexible($query, $key, $bindings, $useReadPdo); - } - - return $this - ->retrieveLock($key) - ->block($this->lockWait, function () use ($query, $bindings, $useReadPdo, $key): array { - [$key => $results, $this->userKey => $list] = $this->retrieveResultsFromCache($key); - - if ($results === null) { - $results = $this->connection->select($query, $bindings, $useReadPdo); - - $this->repository->put($key, $results, $this->ttl); - - // If the user added a user key, we will append this computed key to it and save it. - if ($this->userKey) { - $this->addComputedKeyToUserKey($key, $list); - } - } - - return $results; - }); - } - - /** - * Run a select statement and return a single result. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @return mixed - */ - public function selectOne($query, $bindings = [], $useReadPdo = true) - { - $records = $this->select($query, $bindings, $useReadPdo); - - return array_shift($records); - } - - /** - * Hashes the incoming query for using as cache key. - */ - protected function getQueryHash(string $query, array $bindings): string - { - return isset(static::$queryHasher) - ? (static::$queryHasher)($this->connection, $query, $bindings) - : rtrim(base64_encode(md5($this->connection->getDatabaseName().$query.implode('', $bindings), true)), '='); - } - - /** - * Retrieves the lock to use before getting the results. - */ - protected function retrieveLock(string $key): Lock - { - if (! $this->lockWait) { - return new NoLock($key, $this->lockWait); - } - - // @phpstan-ignore-next-line - return $this->repository->getStore()->lock($key, $this->lockWait); - } - - /** - * Retrieve the results from the cache. - */ - protected function retrieveResultsFromCache(string $key): array - { - // If the ttl is negative, regenerate the results. - if (is_int($this->ttl) && $this->ttl < 1) { - return [$key => null, $this->userKey => null]; - } - - return $this->repository->getMultiple([$key, $this->userKey]); - } - - /** - * Adds the computed key to the user key queries list. - */ - protected function addComputedKeyToUserKey(string $key, ?array $list): void - { - $list['list'][] = $key; - - if ($this->ttl === null) { - $list['expires_at'] = 'never'; - } - - $list['expires_at'] ??= $this->getTimestamp($this->ttl); - - if ($list['expires_at'] === 'never') { - $this->repository->forever($this->userKey, $list); - - return; - } - - $list['expires_at'] = max($this->getTimestamp($this->ttl), $list['expires_at']); - - $this->repository->put($this->userKey, $list, $this->ttl); - } - - /** - * Gets the timestamp for the expiration time. - */ - protected function getTimestamp(DateInterval|DateTimeInterface|array|int $expiration): int - { - if (is_array($expiration)) { - $expiration = $expiration[1]; - } - - if ($expiration instanceof DateTimeInterface) { - return $expiration->getTimestamp(); - } - - if ($expiration instanceof DateInterval) { - return now()->add($expiration)->getTimestamp(); - } - - return now()->addSeconds($expiration)->getTimestamp(); - } - - /** - * Pass-through all properties to the underlying connection. - */ - public function __get(string $name): mixed - { - return $this->connection->{$name}; - } - - /** - * Pass-through all properties to the underlying connection. - */ - public function __set(string $name, mixed $value): void - { - $this->connection->{$name} = $value; - } - - /** - * Pass-through all method calls to the underlying connection. - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call($method, $arguments) - { - return $this->connection->{$method}(...$arguments); - } - - /** - * Returns the results of the query using stale revalidation. - */ - protected function returnResultsUsingFlexible(string $query, string $key, array $bindings, bool $useReadPdo): mixed - { - return $this->repository // @phpstan-ignore-line - ->flexible($key, $this->ttl, function () use ($query, $bindings, $key, $useReadPdo): mixed { - $results = $this->connection->select($query, $bindings, $useReadPdo); - - if ($this->userKey) { - $this->addComputedKeyToUserKey($key, $this->repository->get($this->userKey)); - } - - return $results; - }); - } - - /** - * Create a new CacheAwareProxy instance. - */ - public static function crateNewInstance( - ConnectionInterface $connection, - DateTimeInterface|DateInterval|int|array|null $ttl, - string $key, - int $wait, - ?string $store, - ): static { - // @phpstan-ignore-next-line - return new static( - $connection, - static::store($store, (bool) $wait), - $ttl, - $wait, - config('cache-query.prefix'), - $key - ); - } - - /** - * Returns the store for caching. - */ - protected static function store(?string $store, bool $lockable): Repository - { - $repository = cache()->store($store ?? config('cache-query.store')); - - if ($lockable && ! $repository->getStore() instanceof LockProvider) { - $store ??= cache()->getDefaultDriver(); - - throw new LogicException("The [$store] cache does not support atomic locks."); - } - - return $repository; - } -} diff --git a/src/CacheQueryServiceProvider.php b/src/CacheQueryServiceProvider.php index 05ddb41..ff9a627 100644 --- a/src/CacheQueryServiceProvider.php +++ b/src/CacheQueryServiceProvider.php @@ -3,11 +3,20 @@ namespace Laragear\CacheQuery; use Closure; -use DateInterval; -use DateTimeInterface; +use DateInterval as Interval; +use DateTimeInterface as DateTime; +use Illuminate\Database\ConnectionInterface as DBConnection; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Query\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; +use function app; +use function base64_encode; +use function explode; +use function implode; +use function md5; +use function rtrim; +use function sort; /** * @internal @@ -34,11 +43,11 @@ public function register(): void */ public function boot(): void { - if (! Builder::hasMacro('cache')) { + if (!Builder::hasMacro('cache')) { Builder::macro('cache', $this->macro()); } - if (! EloquentBuilder::hasGlobalMacro('cache')) { + if (!EloquentBuilder::hasGlobalMacro('cache')) { EloquentBuilder::macro('cache', $this->eloquentMacro()); } @@ -50,6 +59,16 @@ public function boot(): void Console\Commands\CacheQuery\Forget::class, ]); } + + Proxy::$queryHasher = static function (DBConnection $connection, string $query, array $bindings): string { + // If the commutative operations is enabled, we will normalize the query and bindings. + if (app('config')->get('cache-query.commutative')) { + $query = Collection::make(explode(' ', $query))->sort()->implode(''); + sort($bindings); + } + + return rtrim(base64_encode(md5($connection->getDatabaseName().$query.implode('', $bindings), true)), '='); + }; } /** @@ -59,22 +78,20 @@ public function boot(): void */ protected function macro(): Closure { - return function ( - DateTimeInterface|DateInterval|int|bool|array|null $ttl = 60, - string $key = '', - ?string $store = null, - int $wait = 0, - ): Builder { + return function (DateTime|Interval|Closure|Cache|int|bool|array|string|null $ttl = 60): Builder { /** @var \Illuminate\Database\Query\Builder $this */ // Avoid re-wrapping the connection into another proxy. - if ($this->connection instanceof CacheAwareConnectionProxy) { // @phpstan-ignore-line + if ($this->connection instanceof Proxy) { // @phpstan-ignore-line $this->connection = $this->connection->connection; } - $this->connection = CacheAwareConnectionProxy::crateNewInstance( - $this->connection, $ttl === false ? -1 : $ttl, $key, $wait, $store - ); + // Normalize the TTL argument to a Cache instance. + $this->connection = Proxy::crateNewInstance($this->connection, match (true) { + $ttl instanceof Closure => $ttl(new Cache), + !$ttl instanceof Cache => (new Cache)->ttl($ttl), + default => $ttl + }); return $this; }; @@ -87,24 +104,16 @@ protected function macro(): Closure */ protected function eloquentMacro(): Closure { - return function ( - DateTimeInterface|DateInterval|int|bool|array|null $ttl = 60, - string $key = '', - ?string $store = null, - int $wait = 0, - ): EloquentBuilder { - /** - * @var \Illuminate\Database\Eloquent\Builder $this - * - * @phpstan-ignore-next-line - */ - $this->getQuery()->cache($ttl, $key, $store, $wait); - - // This global scope is responsible for caching eager loaded relations. - $this->withGlobalScope( - Scopes\CacheRelations::class, - new Scopes\CacheRelations($ttl === false ? -1 : $ttl, $key, $store, $wait) - ); + return function (DateTime|Interval|Closure|Cache|int|bool|array|string|null $ttl = 60): EloquentBuilder { + /** @var \Illuminate\Database\Eloquent\Builder $this */ + $this->getQuery()->cache($ttl); // @phpstan-ignore-line + + $cache = $this->getQuery()->getConnection()->getCacheHelperInstance(); // @phpstan-ignore-line + + if ($cache->saveNestedQueries) { + // This global scope is responsible for caching eager loaded relations. If the + $this->withGlobalScope(Scopes\CacheRelations::class, new Scopes\CacheRelations($cache)); + } return $this; }; diff --git a/src/Proxy.php b/src/Proxy.php new file mode 100644 index 0000000..1954025 --- /dev/null +++ b/src/Proxy.php @@ -0,0 +1,249 @@ +cache->key) { + $this->cache->key = Str::start($this->cache->key, $this->cachePrefix.'|'); + } + } + + /** + * Returns the Cache Helper instance. + * + * @return \Laragear\CacheQuery\Cache + */ + public function getCacheHelperInstance(): Cache + { + return $this->cache; + } + + /** + * Run a select statement against the database. + * + * @param string $query + * @param array $bindings + * @param bool $useReadPdo + * + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function select($query, $bindings = [], $useReadPdo = true): mixed + { + // Create the unique hash for the query to avoid any duplicate query. + $this->computedKey = (static::$queryHasher)($this->connection, $query, $bindings); + + // We will append the previous related query to the computed key. + if ($this->queryKeySuffix) { + $this->computedKey = $this->queryKeySuffix.'.'.$this->computedKey; + } + + // We will use the prefix to operate on the cache directly. + $key = $this->cachePrefix.'|'.$this->computedKey; + + // If the user is setting an array, we will steer return the results using "flexible". + if (is_array($this->cache->ttl) && method_exists($this->repository, 'flexible')) { + return $this->retrieveFlexibleResults($query, $key, $bindings, $useReadPdo); + } + + return $this->retrieveResults($query, $key, $bindings, $useReadPdo); + } + + /** + * Run a select statement and return a single result. + * + * @param string $query + * @param array $bindings + * @param bool $useReadPdo + * @return mixed + * + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function selectOne($query, $bindings = [], $useReadPdo = true): mixed + { + $records = $this->select($query, $bindings, $useReadPdo); + + return array_shift($records); + } + + /** + * Returns the results of the query using stale revalidation. + */ + protected function retrieveFlexibleResults(string $query, string $key, array $bindings, bool $useReadPdo): mixed + { + // @phpstan-ignore-next-line + return $this->repository->flexible( + $key, + $this->cache->ttl, + function () use ($query, $bindings, $key, $useReadPdo): array { + return $this->retrieveResults($query, $key, $bindings, $useReadPdo); + }, + $this->cache->lock, + ); + } + + /** + * Retrieves the results normally from the cache store. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + protected function retrieveResults(string $query, string $key, array $bindings, bool $useReadPdo): array + { + [$key => $results, $this->cache->key => $list] = $this->retrieveResultsFromCache($key); + + // If there are no results for the cache, retrieve the results from the database. + if ($results === null) { + $results = $this->connection->select($query, $bindings, $useReadPdo); + + // If the results are empty, we will NOT save it if the developer instructed so. + if ($results !== [] || $this->cache->saveEmptyResults) { + $this->repository->put($key, $results, $this->cache->ttl); + + // If the user added a user key, we will append this computed key to it and save it. + if ($this->cache->key) { + $this->addComputedKeyToUserKey($key, $list); + } + } + } + + return $results; + } + + /** + * Retrieve the results from the cache. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + protected function retrieveResultsFromCache(string $key): array + { + // If the cache should be regenerated, just return empty results. + if ($this->cache->regenFactor === (bool) value($this->cache->regenerate)) { + return [$key => null, $this->cache->key => null]; + } + + return $this->repository->getMultiple([$key, $this->cache->key]); + } + + /** + * Adds the computed key to the user key queries list. + */ + protected function addComputedKeyToUserKey(string $key, ?array $list): void + { + $list['list'][] = $key; + + if ($this->cache->ttl === null) { + $list['expires_at'] = 'never'; + } + + $list['expires_at'] ??= $this->getTimestamp($this->cache->ttl); + + if ($list['expires_at'] === 'never') { + $this->repository->forever($this->cache->key, $list); + } else { + $list['expires_at'] = max($this->getTimestamp($this->cache->ttl), $list['expires_at']); + $this->repository->put($this->cache->key, $list, $this->cache->ttl); + } + } + + /** + * Gets the timestamp for the expiration time. + */ + protected function getTimestamp(DateInterval|DateTimeInterface|array|int $expiration): int + { + if (is_array($expiration)) { + $expiration = $expiration[1]; + } + + if ($expiration instanceof DateTimeInterface) { + return $expiration->getTimestamp(); + } + + if ($expiration instanceof DateInterval) { + return now()->add($expiration)->getTimestamp(); + } + + return now()->addSeconds($expiration)->getTimestamp(); + } + + /** + * Pass-through all properties to the underlying connection. + */ + public function __get(string $name): mixed + { + return $this->connection->{$name}; + } + + /** + * Pass-through all properties to the underlying connection. + */ + public function __set(string $name, mixed $value): void + { + $this->connection->{$name} = $value; + } + + /** + * Pass-through all method calls to the underlying connection. + * + * @param string $method + * @param array $parameters + * @return mixed + * @codeCoverageIgnore + */ + public function __call($method, $parameters) + { + return $this->connection->{$method}(...$parameters); + } + + /** + * Create a new CacheAwareProxy instance. + */ + public static function crateNewInstance(ConnectionInterface $connection, Cache $cache): static + { + $config = app('config'); + + // @phpstan-ignore-next-line + return new static( + $connection, + app('cache')->store($cache->store ?? $config->get('cache-query.store')), + $cache, + $config->get('cache-query.prefix') + ); + } +} diff --git a/src/Scopes/CacheRelations.php b/src/Scopes/CacheRelations.php index f40b956..6a26cad 100644 --- a/src/Scopes/CacheRelations.php +++ b/src/Scopes/CacheRelations.php @@ -2,27 +2,14 @@ namespace Laragear\CacheQuery\Scopes; -use DateInterval; -use DateTimeInterface; use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilderContract; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; +use Laragear\CacheQuery\Cache; class CacheRelations implements Scope { - /** - * Creates a new scope instance. - */ - public function __construct( - protected DateTimeInterface|DateInterval|int|array|null $ttl, - protected string $key, - protected ?string $store, - protected int $wait, - ) { - // - } - /** * Apply the scope to a given Eloquent query builder. */ @@ -38,8 +25,7 @@ public function apply(Builder $builder, Model $model): void $callback($eloquent); // Always override the previous eloquent builder with the base cache parameters. - // @phpstan-ignore-next-line - $eloquent->cache($this->ttl, $this->key, $this->store, $this->wait); + $eloquent->cache($builder->getConnection()->getCacheHelperInstance()); // @phpstan-ignore-line // @phpstan-ignore-next-line $eloquent->getConnection()->queryKeySuffix = $builder->getConnection()->computedKey; diff --git a/tests/CacheTest.php b/tests/CacheTest.php new file mode 100644 index 0000000..a0db872 --- /dev/null +++ b/tests/CacheTest.php @@ -0,0 +1,197 @@ +ttl); + } + + public function test_uses_ttl_as_integer(): void + { + $cache = Cache::for(30); + + static::assertSame(30, $cache->ttl); + } + + public function test_uses_ttl_as_datetime_interface(): void + { + $datetime = now(); + + $cache = Cache::for($datetime); + + static::assertSame($datetime, $cache->ttl); + } + + public function test_uses_ttl_as_date_interval(): void + { + $interval = new CarbonInterval(); + + $cache = Cache::for($interval); + + static::assertSame($interval, $cache->ttl); + } + + public function test_uses_ttl_as_numeric_string_throws_exception(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The $ttl argument can only be "ever" or "forever" or "null".'); + + Cache::for('10'); + } + + public function test_uses_ttl_as_ever_string(): void + { + $cache = Cache::for('ever'); + + static::assertSame(null, $cache->ttl); + } + + public function test_uses_ttl_as_forever_string(): void + { + $cache = Cache::for('forever'); + + static::assertSame(null, $cache->ttl); + } + + public function test_uses_ttl_as_array(): void + { + $cache = Cache::for([100, 200]); + + static::assertSame([100, 200], $cache->ttl); + } + + public function test_uses_ttl_as_null(): void + { + $cache = Cache::for(null); + + static::assertNull($cache->ttl); + } + + public function test_sets_store(): void + { + $cache = Cache::for(60); + + static::assertNull($cache->store); + + $cache->store('test'); + + static::assertSame('test', $cache->store); + } + + public function test_except_empty(): void + { + $cache = Cache::for(50); + + static::assertTrue($cache->saveEmptyResults); + + $cache->exceptEmpty(); + + static::assertFalse($cache->saveEmptyResults); + } + + public function test_except_nested(): void + { + $cache = Cache::for(60); + + static::assertTrue($cache->saveNestedQueries); + + $cache->exceptNested(); + + static::assertFalse($cache->saveNestedQueries); + } + + public function test_flexible(): void + { + $cache = Cache::flexible(20, 30); + + static::assertSame([20, 30], $cache->ttl); + static::assertNull($cache->lock); + } + + public function test_flexible_with_lock(): void + { + $cache = Cache::flexible(20, 30, [60, 'owner_test']); + + static::assertSame([20, 30], $cache->ttl); + static::assertSame([60, 'owner_test'], $cache->lock); + } + + public function test_regen_when(): void + { + $cache = Cache::for(60); + + $cache->regenWhen($condition = fn() => true); + + static::assertTrue($cache->regenFactor); + static::assertSame($condition, $cache->regenerate); + } + + public function test_regen_if(): void + { + $cache = Cache::for(60); + + $cache->regenIf($condition = fn() => true); + + static::assertTrue($cache->regenFactor); + static::assertSame($condition, $cache->regenerate); + } + + public function test_regen_unless(): void + { + $cache = Cache::for(60); + + $cache->regenUnless($condition = fn() => false); + + static::assertFalse($cache->regenFactor); + static::assertSame($condition, $cache->regenerate); + } + + public function test_as(): void + { + $cache = Cache::for(60); + + static::assertEmpty($cache->key); + + $cache->as('test'); + + static::assertSame('test', $cache->key); + } + + public function test_ever(): void + { + $cache = Cache::for(60); + + $cache->ever(); + + static::assertNull($cache->ttl); + } + + public function test_ttl(): void + { + $cache = Cache::for(60); + + $cache->ttl(30); + + static::assertSame(30, $cache->ttl); + } + + public function test_until(): void + { + $cache = Cache::for(60); + + $cache->until(30); + + static::assertSame(30, $cache->ttl); + } +} diff --git a/tests/CacheAwareConnectionProxyTest.php b/tests/ProxyTest.php similarity index 72% rename from tests/CacheAwareConnectionProxyTest.php rename to tests/ProxyTest.php index b35a6f8..438a447 100644 --- a/tests/CacheAwareConnectionProxyTest.php +++ b/tests/ProxyTest.php @@ -2,6 +2,7 @@ namespace Tests; +use Carbon\CarbonInterval; use Closure; use Illuminate\Cache\Repository as CacheRepository; use Illuminate\Contracts\Cache\Lock; @@ -14,7 +15,8 @@ use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Laragear\CacheQuery\CacheAwareConnectionProxy; +use Laragear\CacheQuery\Cache; +use Laragear\CacheQuery\Proxy; use LogicException; use Mockery; use Orchestra\Testbench\Attributes\WithMigration; @@ -26,7 +28,7 @@ use function today; #[WithMigration] -class CacheAwareConnectionProxyTest extends TestCase +class ProxyTest extends TestCase { use WithFaker; @@ -48,8 +50,6 @@ protected function setUp(): void if (method_exists($this, 'withoutDefer')) { $this->withoutDefer(); } - - CacheAwareConnectionProxy::$queryHasher = null; }); parent::setUp(); @@ -75,6 +75,13 @@ protected function defineDatabaseMigrations(): void }); } + public function test_passes_through_method_calls(): void + { + $query = $this->app->make('db')->table('users')->cache()->where('id', 1); + + static::assertSame([], $query->getConnection()->getQueryLog()); + } + public function test_caches_base_query_into_default_store(): void { $get = $this->app->make('db')->table('users')->cache()->where('id', 1)->get(); @@ -101,6 +108,36 @@ public function test_caches_eloquent_query_into_default_store(): void static::assertEquals($first, $second); } + public function test_caches_base_query_for_sixty_seconds(): void + { + $hash = 'cache-query|X/UPpOGQDQSgAtjm14OWzw'; + + $this->freezeSecond(); + + $this->app->make('db')->table('users')->cache()->where('id', 1)->get(); + + static::assertNotNull($this->app->make('cache')->store()->get($hash)); + + $this->travelTo(now()->addSeconds(61)); + + static::assertNull($this->app->make('cache')->store()->get($hash)); + } + + public function test_caches_eloquent_query_for_sixty_seconds(): void + { + $hash = 'cache-query|X/UPpOGQDQSgAtjm14OWzw'; + + $this->freezeSecond(); + + User::query()->cache()->where('id', 1)->get(); + + static::assertNotNull($this->app->make('cache')->store()->get($hash)); + + $this->travelTo(now()->addSeconds(61)); + + static::assertNull($this->app->make('cache')->store()->get($hash)); + } + public function test_cached_base_query_returns_cached_results_from_same_query(): void { $first = $this->app->make('db')->table('users')->cache()->where('id', 1)->first(); @@ -147,6 +184,26 @@ public function test_cached_eloquent_query_stores_empty_array_and_null_results() static::assertTrue($this->app->make('cache')->store()->has($hash)); } + public function test_cached_base_query_does_not_stores_empty_array_and_null_results(): void + { + $hash = 'cache-query|6SHtUJfPv2GbKc4ikp7cLQ'; + + $null = $this->app->make('db')->table('users')->cache(Cache::for(30)->exceptEmpty())->where('id', 11)->first(); + + static::assertNull($null); + static::assertFalse($this->app->make('cache')->store()->has($hash)); + } + + public function test_cached_eloquent_query_does_not_store_empty_array_and_null_results(): void + { + $hash = 'cache-query|6SHtUJfPv2GbKc4ikp7cLQ'; + + $null = User::query()->cache(Cache::for(30)->exceptEmpty())->where('id', 11)->first(); + + static::assertNull($null); + static::assertFalse($this->app->make('cache')->store()->has($hash)); + } + public function test_cached_base_query_doesnt_intercepts_manually_cached_null_values(): void { $this->app->make('db')->table('users')->insert([ @@ -205,6 +262,19 @@ public function test_cached_eloquent_query_hash_differs_when_columns_are_differe static::assertNull($second); } + public function test_cached_base_query_hash_is_commutative(): void + { + $this->app->make('config')->set('cache-query.commutative', true); + + $first = $this->app->make('db')->table('users')->cache()->whereNotNull('name')->where('id', 1)->first(); + + $this->app->make('db')->table('users')->where('id', 1)->delete(); + + $second = $this->app->make('db')->table('users')->cache()->where('id', 1)->whereNotNull('name')->first(); + + static::assertEquals($first, $second); + } + public function test_cached_base_query_works_as_before_last_method_with_different_columns(): void { $this->app->make('db')->table('users')->where('id', 1)->cache()->first(['name']); @@ -326,53 +396,11 @@ public function test_uses_custom_time_to_live(): void $this->app->make('db')->table('users')->cache($interval)->first(); } - public function test_exception_if_repository_store_is_not_lockable_when_waiting(): void - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The [foo] cache does not support atomic locks.'); - - $store = $this->spy(Repository::class); - - $cache = $this->mock('cache'); - - $cache->allows('store')->with(null)->andReturn($store); - $cache->allows('getDefaultDriver')->andReturn('foo'); - - $this->app->make('db')->table('users')->cache(wait: 30)->first(); - } - - public function test_locks_cache_when_waiting(): void - { - $hash = 'cache-query|30250dGAv64n2ySOIxuL+g'; - - $lock = $this->mock(Lock::class); - $lock->expects('block')->withArgs(function ($time, $callback): bool { - static::assertSame(30, $time); - static::assertInstanceOf(Closure::class, $callback); - - $callback(); - - return true; - })->andReturnFalse(); - - $store = $this->mock(LockProvider::class); - $store->expects('lock')->with($hash, 30)->andReturn($lock); - - $repository = $this->mock(Repository::class); - $repository->expects('getMultiple')->with([$hash, ''])->andReturn(['' => null, $hash => null]); - $repository->expects('getStore')->withNoArgs()->twice()->andReturn($store); - $repository->expects('put')->with($hash, Mockery::type('array'), 60); - - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); - - $this->app->make('db')->table('users')->cache(wait: 30)->first(); - } - public function test_saves_user_key_with_real_computed_keys_list(): void { $this->travelTo(now()); - $this->app->make('db')->table('users')->cache(key: 'foo')->first(); + $this->app->make('db')->table('users')->cache(Cache::for(60)->as('foo'))->first(); static::assertTrue($this->app->make('cache')->has('cache-query|30250dGAv64n2ySOIxuL+g')); static::assertSame( @@ -383,8 +411,8 @@ public function test_saves_user_key_with_real_computed_keys_list(): void public function test_first_query_takes_precedence_over_second_query_with_different_key(): void { - $this->app->make('db')->table('users')->cache(key: 'foo')->first(); - $this->app->make('db')->table('users')->cache(key: 'bar')->first(); + $this->app->make('db')->table('users')->cache(Cache::for(60)->as('foo'))->first(); + $this->app->make('db')->table('users')->cache(Cache::for(60)->as('bar'))->first(); static::assertTrue($this->app->make('cache')->has('cache-query|foo')); static::assertFalse($this->app->make('cache')->has('cache-query|bar')); @@ -394,14 +422,14 @@ public function test_largest_ttl_key_takes_precedence(): void { $this->travelTo(now()->startOfSecond()); - $this->app->make('db')->table('users')->where('id', 1)->cache(ttl: 120, key: 'foo')->first(); - $this->app->make('db')->table('users')->where('id', 1)->cache(ttl: 30, key: 'foo')->first(); + $this->app->make('db')->table('users')->where('id', 1)->cache(Cache::for(120)->as('foo'))->first(); + $this->app->make('db')->table('users')->where('id', 1)->cache(Cache::for(30)->as('foo'))->first(); - $this->app->make('db')->table('users')->where('id', 2)->cache(ttl: now()->addSeconds(120), key: 'bar')->first(); - $this->app->make('db')->table('users')->where('id', 2)->cache(ttl: now()->addSeconds(30), key: 'bar')->first(); + $this->app->make('db')->table('users')->where('id', 2)->cache(Cache::for(now()->addSeconds(120))->as('bar'))->first(); + $this->app->make('db')->table('users')->where('id', 2)->cache(Cache::for(now()->addSeconds(30))->as('bar'))->first(); - $this->app->make('db')->table('users')->where('id', 4)->cache(ttl: null, key: 'quz')->first(); - $this->app->make('db')->table('users')->where('id', 4)->cache(ttl: 30, key: 'quz')->first(); + $this->app->make('db')->table('users')->where('id', 4)->cache(Cache::for(null)->as('quz'))->first(); + $this->app->make('db')->table('users')->where('id', 4)->cache(Cache::for(30)->as('quz'))->first(); $this->travelTo(now()->addMinute()); @@ -423,24 +451,48 @@ public function test_largest_ttl_key_takes_precedence(): void static::assertTrue($this->app->make('cache')->has('cache-query|quz')); } - public function test_regenerates_cache_using_false_ttl(): void + public function test_regenerates_cache_using_regen_when(): void { $this->app->make('db')->table('users')->where('id', 1)->cache()->first(); $this->app->make('db')->table('users')->where('id', 1)->update(['name' => 'test']); - $result = $this->app->make('db')->table('users')->where('id', 1)->cache(false)->first(); + $result = $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(Cache::for(60)->regenWhen(false)) + ->first(); + + static::assertNotSame('test', $result->name); + + $result = $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(Cache::for(60)->regenWhen(true)) + ->first(); static::assertSame('test', $result->name); } - public function test_regenerates_cache_using_ttl_with_negative_number(): void + public function test_regenerates_cache_using_regen_unless(): void { $this->app->make('db')->table('users')->where('id', 1)->cache()->first(); $this->app->make('db')->table('users')->where('id', 1)->update(['name' => 'test']); - $result = $this->app->make('db')->table('users')->where('id', 1)->cache(-1)->first(); + $result = $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(Cache::for(60)->regenUnless(true)) + ->first(); + + static::assertNotSame('test', $result->name); + + $result = $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(Cache::for(60)->regenUnless(false)) + ->first(); static::assertSame('test', $result->name); } @@ -455,14 +507,81 @@ public function test_uses_db_flexible_caching_when_using_ttl_as_array_of_values( $repository = $this->mock(CacheRepository::class); $repository->expects('put')->never(); - $repository->expects('flexible')->with($hash, [5, 300], Mockery::type('\Closure'))->once(); + $repository->expects('flexible')->with($hash, [5, 300], Mockery::type('\Closure'), null)->once(); $repository->expects('getMultiple')->never(); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); $this->app->make('db')->table('users')->where('id', 1)->cache([5, 300])->first(); } + public function test_doesnt_check_if_the_flexible_array_is_well_formed(): void + { + if (! method_exists(CacheRepository::class, 'flexible')) { + $this->markTestSkipped('Cannot test flexible caching if repository does not implements it.'); + } + + $malformed = []; + + $hash = 'cache-query|fj8Xyz4K1Zh0tdAamPbG1A'; + + $repository = $this->mock(CacheRepository::class); + $repository->expects('put')->never(); + $repository->expects('flexible')->with($hash, $malformed, Mockery::type('\Closure'), null)->once(); + $repository->expects('getMultiple')->never(); + + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); + + $this->app->make('db')->table('users')->where('id', 1)->cache($malformed)->first(); + } + + public function test_uses_date_interval_with_user_key(): void + { + $this->freezeSecond(); + + $hash = 'cache-query|fj8Xyz4K1Zh0tdAamPbG1A'; + + $interval = CarbonInterval::make(60, 'seconds'); + + $repository = $this->mock(Repository::class); + $repository->expects('flexible')->never(); + $repository->expects('put')->with($hash, Mockery::type('array'), $interval)->once(); + $repository->expects('put')->with( + 'cache-query|some-key', + ['list' => [$hash], 'expires_at' => now()->add($interval)->getTimestamp()], + $interval + )->once(); + $repository->expects('getMultiple') + ->with([$hash, 'cache-query|some-key']) + ->times(1) + ->andReturn(['cache-query|some-key' => null, $hash => null]); + + + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); + + $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(fn($cache) => $cache->ttl($interval)->as('some-key'))->first(); + } + + public function test_uses_custom_store(): void + { + $hash = 'cache-query|fj8Xyz4K1Zh0tdAamPbG1A'; + + $repository = $this->mock(Repository::class); + $repository->expects('flexible')->never(); + $repository->expects('put')->with($hash, Mockery::type('array'), 60)->once(); + $repository->expects('getMultiple')->with([$hash, ''])->times(1)->andReturn(['' => null, $hash => null]); + + $this->mock('cache')->expects('store')->with('test-store')->andReturn($repository); + + $this->app->make('db') + ->table('users') + ->where('id', 1) + ->cache(fn($cache) => $cache->store('test-store'))->first(); + } + public function test_uses_eloquent_flexible_caching_when_using_ttl_as_array_of_values(): void { if (! method_exists(CacheRepository::class, 'flexible')) { @@ -473,24 +592,42 @@ public function test_uses_eloquent_flexible_caching_when_using_ttl_as_array_of_v $repository = $this->mock(CacheRepository::class); $repository->expects('put')->never(); - $repository->expects('flexible')->with($hash, [5, 300], Mockery::type('\Closure'))->once(); + $repository->expects('flexible')->with($hash, [5, 300], Mockery::type('\Closure'), null)->once(); $repository->expects('getMultiple')->never(); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); User::where('id', 1)->cache([5, 300])->first(); } + public function test_uses_eloquent_flexible_caching_with_lock_arguments(): void + { + if (! method_exists(CacheRepository::class, 'flexible')) { + $this->markTestSkipped('Cannot test flexible caching if repository does not implements it.'); + } + + $hash = 'cache-query|fj8Xyz4K1Zh0tdAamPbG1A'; + + $repository = $this->mock(CacheRepository::class); + $repository->expects('put')->never(); + $repository->expects('flexible')->with($hash, [5, 300], Mockery::type('\Closure'), ['test', 10])->once(); + $repository->expects('getMultiple')->never(); + + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); + + User::where('id', 1)->cache(Cache::flexible(5, 300, ['test', 10]))->first(); + } + public function test_flexible_cache_uses_user_key(): void { - $cached = User::query()->cache(key: 'foo', ttl: [5, 300])->with('posts', function ($posts) { + $cached = User::query()->cache(Cache::for([5, 300])->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2); })->whereKey(1)->first(); User::query()->whereKey(1)->delete(); Post::query()->whereKey(2)->delete(); - $renewed = User::query()->cache(key: 'foo', ttl: [5, 300])->with('posts', function ($posts) { + $renewed = User::query()->cache(Cache::for([5, 300])->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2); })->whereKey(1)->first(); @@ -507,15 +644,15 @@ public function test_doesnt_uses_flexible_caching_if_repository_is_not_flexible( $repository->expects('put')->with($hash, Mockery::type('array'), [5, 300])->once(); $repository->expects('getMultiple')->with([$hash, ''])->times(1)->andReturn(['' => null, $hash => null]); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->with(null)->andReturn($repository); $this->app->make('db')->table('users')->where('id', 1)->cache([5, 300])->first(); } public function test_different_queries_with_same_key_add_to_same_list(): void { - $this->app->make('db')->table('users')->cache(null, 'foo')->where('id', 1)->first(); - $this->app->make('db')->table('users')->cache(null, 'foo')->where('id', 2)->first(); + $this->app->make('db')->table('users')->cache(Cache::for(null)->as('foo'))->where('id', 1)->first(); + $this->app->make('db')->table('users')->cache(Cache::for(null)->as('foo'))->where('id', 2)->first(); static::assertTrue($this->app->make('cache')->has('cache-query|fj8Xyz4K1Zh0tdAamPbG1A')); static::assertTrue($this->app->make('cache')->has('cache-query|u7YzPIzZNGNu7Dkr/kx4Iw')); @@ -560,14 +697,14 @@ public function test_caches_eager_loaded_query(): void public function test_caches_eager_loaded_query_with_user_key(): void { - $cached = User::query()->cache(key: 'foo')->with('posts', function ($posts) { + $cached = User::query()->cache(Cache::for(60)->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2); })->whereKey(1)->first(); User::query()->whereKey(1)->delete(); Post::query()->whereKey(2)->delete(); - $renewed = User::query()->cache(key: 'foo')->with('posts', function ($posts) { + $renewed = User::query()->cache(Cache::for(60)->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2); })->whereKey(1)->first(); @@ -577,7 +714,7 @@ public function test_caches_eager_loaded_query_with_user_key(): void public function test_cached_eager_loaded_query_with_user_key_saves_computed_query_keys_list(): void { - User::query()->cache(null, 'foo')->with('posts', function ($posts) { + User::query()->cache(Cache::for('ever')->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2); })->whereKey(1)->first(); @@ -592,8 +729,8 @@ public function test_cached_eager_loaded_query_with_user_key_saves_computed_quer public function test_overrides_cached_eager_load_query_with_parent_user_keys(): void { - User::query()->cache(null, 'foo')->with('posts', function ($posts) { - $posts->whereKey(2)->cache(key: 'bar'); + User::query()->cache(Cache::for('ever')->as('foo'))->with('posts', function ($posts) { + $posts->whereKey(2)->cache(Cache::for('ever')->as('bar')); })->whereKey(1)->first(); static::assertSame( @@ -631,7 +768,7 @@ public function test_caches_deep_eager_loaded_query(): void public function test_cached_deep_eager_loaded_query_with_user_key_saves_computed_query_keys_list(): void { - User::query()->cache(null, 'foo')->with('posts', function ($posts) { + User::query()->cache(Cache::for('ever')->as('foo'))->with('posts', function ($posts) { $posts->whereKey(2)->with('comments'); })->whereKey(1)->first(); @@ -648,10 +785,30 @@ public function test_cached_deep_eager_loaded_query_with_user_key_saves_computed ); } + public function test_doesnt_caches_eager_loaded_query(): void + { + $this->app->make('db')->table('comments')->insert(['likes' => 1, 'body' => 'test', 'post_id' => 2]); + + $cached = User::query()->cache(Cache::for(60)->exceptNested())->with('posts', function ($posts) { + $posts->whereKey(2)->with('comments'); + })->whereKey(1)->first(); + + User::query()->whereKey(1)->delete(); + Post::query()->whereKey(2)->delete(); + Comment::query()->whereKey(1)->delete(); + + $renewed = User::query()->cache()->with('posts', function ($posts) { + $posts->whereKey(2)->with('comments'); + })->whereKey(1)->first(); + + static::assertTrue($cached->is($renewed)); + static::assertEmpty($renewed->posts); + } + public function test_caches_above_one_level_deep_eager_load_relation_query(): void { User::query()->with('posts', function ($posts) { - $posts->whereKey(2)->cache(null, 'foo')->with('comments'); + $posts->whereKey(2)->cache(Cache::for('ever')->as('foo'))->with('comments'); })->whereKey(1)->first(); static::assertSame( @@ -672,7 +829,7 @@ public function test_calling_to_sql_does_not_cache_result(): void $repository->expects('has')->never(); $repository->expects('put')->never(); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->twice()->with(null)->andReturn($repository); static::assertIsString($this->app->make('db')->table('users')->cache()->toSql()); static::assertIsString(User::query()->cache()->toSql()); @@ -684,7 +841,7 @@ public function test_calling_non_executing_method_doesnt_caches_the_result(): vo $repository->expects('has')->never(); $repository->expects('put')->never(); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->twice()->with(null)->andReturn($repository); static::assertIsArray($this->app->make('db')->table('users')->cache()->getBindings()); static::assertIsArray(User::query()->cache()->getBindings()); @@ -696,7 +853,7 @@ public function test_calling_non_returning_builder_method_does_not_cache_result( $repository->expects('has')->never(); $repository->expects('put')->never(); - $this->mock('cache')->shouldReceive('store')->with(null)->andReturn($repository); + $this->mock('cache')->expects('store')->once()->with(null)->andReturn($repository); static::assertIsArray(User::query()->cache()->with('pages')->getEagerLoads()); } @@ -707,7 +864,7 @@ public function test_no_exception_when_caching_eloquent_query_twice(): void $proxy = $builder->getConnection(); - static::assertInstanceOf(CacheAwareConnectionProxy::class, $proxy); + static::assertInstanceOf(Proxy::class, $proxy); static::assertInstanceOf(ConnectionInterface::class, $proxy->connection); $sameConnection = $builder->cache()->getConnection(); @@ -748,7 +905,7 @@ public function test_select_one_uses_cache(): void public function test_sets_custom_query_hasher(): void { - CacheAwareConnectionProxy::$queryHasher = function ( + Proxy::$queryHasher = function ( ConnectionInterface $connection, string $query, array $bindings @@ -760,7 +917,7 @@ public function test_sets_custom_query_hasher(): void return 'test_hash'; }; - User::query()->cache('foo')->whereKey(1)->first(); + User::query()->cache()->whereKey(1)->first(); static::assertTrue($this->app->make('cache')->has('cache-query|test_hash')); } From 1294fd61f1cb891080d97313c6f128e0be6abb7e Mon Sep 17 00:00:00 2001 From: Italo Date: Wed, 12 Feb 2025 05:34:27 +0000 Subject: [PATCH 02/10] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Cache.php | 1 + src/CacheQueryServiceProvider.php | 7 ++++--- src/Proxy.php | 2 ++ tests/CacheTest.php | 7 ++++--- tests/ProxyTest.php | 9 ++------- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Cache.php b/src/Cache.php index e22746c..8ee3e50 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -6,6 +6,7 @@ use DateTimeInterface; use Illuminate\Support\Str; use InvalidArgumentException; + use function in_array; use function is_numeric; use function is_string; diff --git a/src/CacheQueryServiceProvider.php b/src/CacheQueryServiceProvider.php index ff9a627..8d2b00f 100644 --- a/src/CacheQueryServiceProvider.php +++ b/src/CacheQueryServiceProvider.php @@ -10,6 +10,7 @@ use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; + use function app; use function base64_encode; use function explode; @@ -43,11 +44,11 @@ public function register(): void */ public function boot(): void { - if (!Builder::hasMacro('cache')) { + if (! Builder::hasMacro('cache')) { Builder::macro('cache', $this->macro()); } - if (!EloquentBuilder::hasGlobalMacro('cache')) { + if (! EloquentBuilder::hasGlobalMacro('cache')) { EloquentBuilder::macro('cache', $this->eloquentMacro()); } @@ -89,7 +90,7 @@ protected function macro(): Closure // Normalize the TTL argument to a Cache instance. $this->connection = Proxy::crateNewInstance($this->connection, match (true) { $ttl instanceof Closure => $ttl(new Cache), - !$ttl instanceof Cache => (new Cache)->ttl($ttl), + ! $ttl instanceof Cache => (new Cache)->ttl($ttl), default => $ttl }); diff --git a/src/Proxy.php b/src/Proxy.php index 1954025..5afc7f4 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Str; + use function app; use function array_shift; use function is_array; @@ -224,6 +225,7 @@ public function __set(string $name, mixed $value): void * @param string $method * @param array $parameters * @return mixed + * * @codeCoverageIgnore */ public function __call($method, $parameters) diff --git a/tests/CacheTest.php b/tests/CacheTest.php index a0db872..86ea5e1 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Laragear\CacheQuery\Cache; use PHPUnit\Framework\TestCase as BaseTestCase; + use function now; class CacheTest extends BaseTestCase @@ -131,7 +132,7 @@ public function test_regen_when(): void { $cache = Cache::for(60); - $cache->regenWhen($condition = fn() => true); + $cache->regenWhen($condition = fn () => true); static::assertTrue($cache->regenFactor); static::assertSame($condition, $cache->regenerate); @@ -141,7 +142,7 @@ public function test_regen_if(): void { $cache = Cache::for(60); - $cache->regenIf($condition = fn() => true); + $cache->regenIf($condition = fn () => true); static::assertTrue($cache->regenFactor); static::assertSame($condition, $cache->regenerate); @@ -151,7 +152,7 @@ public function test_regen_unless(): void { $cache = Cache::for(60); - $cache->regenUnless($condition = fn() => false); + $cache->regenUnless($condition = fn () => false); static::assertFalse($cache->regenFactor); static::assertSame($condition, $cache->regenerate); diff --git a/tests/ProxyTest.php b/tests/ProxyTest.php index 438a447..b0f5602 100644 --- a/tests/ProxyTest.php +++ b/tests/ProxyTest.php @@ -3,10 +3,7 @@ namespace Tests; use Carbon\CarbonInterval; -use Closure; use Illuminate\Cache\Repository as CacheRepository; -use Illuminate\Contracts\Cache\Lock; -use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Eloquent\Model; @@ -17,7 +14,6 @@ use Illuminate\Support\Collection; use Laragear\CacheQuery\Cache; use Laragear\CacheQuery\Proxy; -use LogicException; use Mockery; use Orchestra\Testbench\Attributes\WithMigration; @@ -556,13 +552,12 @@ public function test_uses_date_interval_with_user_key(): void ->times(1) ->andReturn(['cache-query|some-key' => null, $hash => null]); - $this->mock('cache')->expects('store')->with(null)->andReturn($repository); $this->app->make('db') ->table('users') ->where('id', 1) - ->cache(fn($cache) => $cache->ttl($interval)->as('some-key'))->first(); + ->cache(fn ($cache) => $cache->ttl($interval)->as('some-key'))->first(); } public function test_uses_custom_store(): void @@ -579,7 +574,7 @@ public function test_uses_custom_store(): void $this->app->make('db') ->table('users') ->where('id', 1) - ->cache(fn($cache) => $cache->store('test-store'))->first(); + ->cache(fn ($cache) => $cache->store('test-store'))->first(); } public function test_uses_eloquent_flexible_caching_when_using_ttl_as_array_of_values(): void From 3c87f618dcffdd604f6767b317e9b3244157c2c9 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:37:31 -0300 Subject: [PATCH 03/10] Should fix static analysis. --- .gitattributes | 1 + phpstan.neon | 3 +++ src/Cache.php | 1 - src/CacheQueryServiceProvider.php | 7 +++---- 4 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 phpstan.neon diff --git a/.gitattributes b/.gitattributes index e509729..8da2d2e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,3 +10,4 @@ /phpunit.xml export-ignore /tests export-ignore /.editorconfig export-ignore +/phpstan.neon export-ignore diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..27153d5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + ignoreErrors: + - '#Unsafe usage of new static().#' diff --git a/src/Cache.php b/src/Cache.php index 8ee3e50..08b15b4 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -219,7 +219,6 @@ public static function for(DateTimeInterface|DateInterval|int|array|null|string * Regenerates the cached results before a specific amount of seconds before the data dies. * * @param array{ seconds?: int, owner?: string }|null $lock - * @return $this */ public static function flexible(int $seconds, int $stale, ?array $lock = null): static { diff --git a/src/CacheQueryServiceProvider.php b/src/CacheQueryServiceProvider.php index 8d2b00f..048fc2f 100644 --- a/src/CacheQueryServiceProvider.php +++ b/src/CacheQueryServiceProvider.php @@ -109,11 +109,10 @@ protected function eloquentMacro(): Closure /** @var \Illuminate\Database\Eloquent\Builder $this */ $this->getQuery()->cache($ttl); // @phpstan-ignore-line - $cache = $this->getQuery()->getConnection()->getCacheHelperInstance(); // @phpstan-ignore-line - - if ($cache->saveNestedQueries) { + // @phpstan-ignore-next-line + if ($this->getQuery()->getConnection()->getCacheHelperInstance()->saveNestedQueries) { // This global scope is responsible for caching eager loaded relations. If the - $this->withGlobalScope(Scopes\CacheRelations::class, new Scopes\CacheRelations($cache)); + $this->withGlobalScope(Scopes\CacheRelations::class, new Scopes\CacheRelations()); } return $this; From c2e46e575a51193e1baa2eff8c1af9a6d03d8746 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:39:41 -0300 Subject: [PATCH 04/10] Should fix static analysis 2 --- .gitattributes | 1 - phpstan.neon | 3 --- src/Cache.php | 1 + src/CacheQueryServiceProvider.php | 9 ++++----- 4 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 phpstan.neon diff --git a/.gitattributes b/.gitattributes index 8da2d2e..e509729 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,3 @@ /phpunit.xml export-ignore /tests export-ignore /.editorconfig export-ignore -/phpstan.neon export-ignore diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index 27153d5..0000000 --- a/phpstan.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - ignoreErrors: - - '#Unsafe usage of new static().#' diff --git a/src/Cache.php b/src/Cache.php index 08b15b4..1385f26 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -11,6 +11,7 @@ use function is_numeric; use function is_string; +/** @phpstan-consistent-constructor */ class Cache { /** diff --git a/src/CacheQueryServiceProvider.php b/src/CacheQueryServiceProvider.php index 048fc2f..cd3cc9f 100644 --- a/src/CacheQueryServiceProvider.php +++ b/src/CacheQueryServiceProvider.php @@ -10,7 +10,6 @@ use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; - use function app; use function base64_encode; use function explode; @@ -44,11 +43,11 @@ public function register(): void */ public function boot(): void { - if (! Builder::hasMacro('cache')) { + if (!Builder::hasMacro('cache')) { Builder::macro('cache', $this->macro()); } - if (! EloquentBuilder::hasGlobalMacro('cache')) { + if (!EloquentBuilder::hasGlobalMacro('cache')) { EloquentBuilder::macro('cache', $this->eloquentMacro()); } @@ -90,7 +89,7 @@ protected function macro(): Closure // Normalize the TTL argument to a Cache instance. $this->connection = Proxy::crateNewInstance($this->connection, match (true) { $ttl instanceof Closure => $ttl(new Cache), - ! $ttl instanceof Cache => (new Cache)->ttl($ttl), + !$ttl instanceof Cache => (new Cache)->ttl($ttl), default => $ttl }); @@ -111,7 +110,7 @@ protected function eloquentMacro(): Closure // @phpstan-ignore-next-line if ($this->getQuery()->getConnection()->getCacheHelperInstance()->saveNestedQueries) { - // This global scope is responsible for caching eager loaded relations. If the + // This global scope is responsible for caching eager loaded relations. $this->withGlobalScope(Scopes\CacheRelations::class, new Scopes\CacheRelations()); } From 67b692977c686067fb8e994598ca1599645c7fda Mon Sep 17 00:00:00 2001 From: Italo Date: Wed, 12 Feb 2025 05:39:50 +0000 Subject: [PATCH 05/10] Apply fixes from StyleCI [ci skip] [skip ci] --- src/CacheQueryServiceProvider.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CacheQueryServiceProvider.php b/src/CacheQueryServiceProvider.php index cd3cc9f..660019f 100644 --- a/src/CacheQueryServiceProvider.php +++ b/src/CacheQueryServiceProvider.php @@ -10,6 +10,7 @@ use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; + use function app; use function base64_encode; use function explode; @@ -43,11 +44,11 @@ public function register(): void */ public function boot(): void { - if (!Builder::hasMacro('cache')) { + if (! Builder::hasMacro('cache')) { Builder::macro('cache', $this->macro()); } - if (!EloquentBuilder::hasGlobalMacro('cache')) { + if (! EloquentBuilder::hasGlobalMacro('cache')) { EloquentBuilder::macro('cache', $this->eloquentMacro()); } @@ -89,7 +90,7 @@ protected function macro(): Closure // Normalize the TTL argument to a Cache instance. $this->connection = Proxy::crateNewInstance($this->connection, match (true) { $ttl instanceof Closure => $ttl(new Cache), - !$ttl instanceof Cache => (new Cache)->ttl($ttl), + ! $ttl instanceof Cache => (new Cache)->ttl($ttl), default => $ttl }); From 49d76aaf0ca08524d2145d3247fd34a40a58e8e3 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:41:48 -0300 Subject: [PATCH 06/10] Should fix package install. --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 262f4ec..e6f114e 100644 --- a/composer.json +++ b/composer.json @@ -18,12 +18,12 @@ }, "require": { "php": "^8.2", - "illuminate/cache": "11.*|10.*", - "illuminate/config": "11.*|10.*", - "illuminate/database": "11.*|10.*", - "illuminate/support": "11.*|10.*", - "illuminate/container": "11.*|10.*", - "illuminate/contracts": "11.*|10.*" + "illuminate/cache": "11.*|12.*", + "illuminate/config": "11.*|12.*", + "illuminate/database": "11.*|12.*", + "illuminate/support": "11.*|12.*", + "illuminate/container": "11.*|12.*", + "illuminate/contracts": "11.*|12.*" }, "require-dev": { "orchestra/testbench": "9.*|10.*" From 5ebf78eb043bc80cff39dbda1e6d99896e5d0075 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:44:33 -0300 Subject: [PATCH 07/10] Sets Codecov to 5.x [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4893ac8..1154673 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Cache Query [![Latest Version on Packagist](https://img.shields.io/packagist/v/laragear/cache-query.svg)](https://packagist.org/packages/laragear/cache-query) [![Latest stable test run](https://github.com/Laragear/CacheQuery/workflows/Tests/badge.svg)](https://github.com/Laragear/CacheQuery/actions) -[![Codecov coverage](https://codecov.io/gh/Laragear/CacheQuery/branch/1.x/graph/badge.svg?token=IOZS1TFJ5G)](https://codecov.io/gh/Laragear/CacheQuery) +[![Codecov coverage](https://codecov.io/gh/Laragear/CacheQuery/branch/5.x/graph/badge.svg?token=IOZS1TFJ5G)](https://codecov.io/gh/Laragear/CacheQuery) [![Maintainability](https://api.codeclimate.com/v1/badges/7e7894f3eee3939333eb/maintainability)](https://codeclimate.com/github/Laragear/CacheQuery/maintainability) [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=Laragear_CacheQuery&metric=alert_status)](https://sonarcloud.io/dashboard?id=Laragear_CacheQuery) [![Laravel Octane Compatibility](https://img.shields.io/badge/Laravel%20Octane-Compatible-success?style=flat&logo=laravel)](https://laravel.com/docs/11.x/octane#introduction) From a4b47c00cb8f780d57f9746c741de02ce7d36ea2 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 02:49:21 -0300 Subject: [PATCH 08/10] Updates codecov action --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 69880f5..99f197d 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -83,7 +83,7 @@ jobs: run: composer run-script test - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 static_analysis: name: 3️⃣ Static Analysis From cbd48567f05320563e65e33b90fad58c8f029490 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 18:13:01 -0300 Subject: [PATCH 09/10] Adds Code Coverage token --- .github/workflows/php.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 99f197d..3328b3f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -84,6 +84,8 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} static_analysis: name: 3️⃣ Static Analysis From fbb38101c36abbaeb921a84efcb4a9752999ad6c Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 12 Feb 2025 18:14:54 -0300 Subject: [PATCH 10/10] Updates Maintainability badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1154673..9dd2edc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest Version on Packagist](https://img.shields.io/packagist/v/laragear/cache-query.svg)](https://packagist.org/packages/laragear/cache-query) [![Latest stable test run](https://github.com/Laragear/CacheQuery/workflows/Tests/badge.svg)](https://github.com/Laragear/CacheQuery/actions) [![Codecov coverage](https://codecov.io/gh/Laragear/CacheQuery/branch/5.x/graph/badge.svg?token=IOZS1TFJ5G)](https://codecov.io/gh/Laragear/CacheQuery) -[![Maintainability](https://api.codeclimate.com/v1/badges/7e7894f3eee3939333eb/maintainability)](https://codeclimate.com/github/Laragear/CacheQuery/maintainability) +[![Maintainability](https://qlty.sh/badges/8738ff13-01ca-4d38-83a8-dd484723093d/maintainability.svg)](https://qlty.sh/gh/Laragear/projects/CacheQuery) [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=Laragear_CacheQuery&metric=alert_status)](https://sonarcloud.io/dashboard?id=Laragear_CacheQuery) [![Laravel Octane Compatibility](https://img.shields.io/badge/Laravel%20Octane-Compatible-success?style=flat&logo=laravel)](https://laravel.com/docs/11.x/octane#introduction)