From 8e8d561da4a41d5007ed9d70fbc1b54ae5c71506 Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sat, 14 Dec 2019 20:01:53 +0200 Subject: [PATCH 1/4] Moved the cache methods in a separate trait. --- src/Contracts/QueryCacheModuleInterface.php | 16 + src/Query/Builder.php | 302 +---------------- src/Traits/QueryCacheModule.php | 356 ++++++++++++++++++++ 3 files changed, 377 insertions(+), 297 deletions(-) create mode 100644 src/Contracts/QueryCacheModuleInterface.php create mode 100644 src/Traits/QueryCacheModule.php diff --git a/src/Contracts/QueryCacheModuleInterface.php b/src/Contracts/QueryCacheModuleInterface.php new file mode 100644 index 0000000..ac4812a --- /dev/null +++ b/src/Contracts/QueryCacheModuleInterface.php @@ -0,0 +1,16 @@ +avoidCache) { + if (! $this->shouldAvoidCache()) { return $this->getFromQueryCache('get', $columns); } return parent::get($columns); } - - /** - * Get the cache from the current query. - * - * @param array $columns - * @return array - */ - protected function getFromQueryCache(string $method = 'get', $columns = ['*']) - { - if (is_null($this->columns)) { - $this->columns = $columns; - } - - $key = $this->getCacheKey('get'); - $cache = $this->getCache(); - $callback = $this->getQueryCacheCallback($method, $columns); - - if ($this->cacheTime instanceof DateTime || $this->cacheTime > 0) { - return $cache->remember($key, $this->cacheTime, $callback); - } - - return $cache->rememberForever($key, $callback); - } - - /** - * Get the query cache callback. - * - * @param string $method - * @param array $columns - * @return \Closure - */ - protected function getQueryCacheCallback(string $method = 'get', $columns = ['*']) - { - return function () use ($method, $columns) { - $this->avoidCache = true; - - return $this->{$method}($columns); - }; - } - - /** - * Get a unique cache key for the complete query. - * - * @param string $method - * @param string|null $id - * @param string|null $appends - * @return string - */ - public function getCacheKey(string $method = 'get', $id = null, $appends = null): string - { - $key = $this->generateCacheKey($method, $id, $appends); - - return "{$this->cachePrefix}:{$key}"; - } - - /** - * Generate the unique cache key for the query. - * - * @param string $method - * @param string|null $id - * @param string|null $appends - * @return string - */ - public function generateCacheKey(string $method = 'get', $id = null, $appends = null): string - { - $key = $this->generatePlainCacheKey($method, $id, $appends); - - if ($this->cacheUsePlainKey) { - return $key; - } - - return hash('sha256', $key); - } - - /** - * Generate the plain unique cache key for the query. - * - * @param string $method - * @param string|null $id - * @param string|null $appends - * @return string - */ - public function generatePlainCacheKey(string $method = 'get', $id = null, $appends = null): string - { - $name = $this->connection->getName(); - - // Count has no Sql, that's why it can't be used ->toSql() - if ($method === 'count') { - return $name.$method.$id.serialize($this->getBindings()).$appends; - } - - return $name.$method.$id.$this->toSql().serialize($this->getBindings()).$appends; - } - - /** - * Flush the cache that contains specific tags. - * - * @param array $tags - * @return bool - */ - public function flushQueryCache(array $tags = []): bool - { - $cache = $this->getCacheDriver(); - - if (! method_exists($cache, 'tags')) { - return false; - } - - foreach ($tags as $tag) { - self::flushQueryCacheWithTag($tag); - } - - return true; - } - - /** - * Flush the cache for a specific tag. - * - * @param string $tag - * @return bool - */ - public function flushQueryCacheWithTag(string $tag): bool - { - $cache = $this->getCacheDriver(); - - if (! method_exists($cache, 'tags')) { - return false; - } - - return $cache->tags($tag)->flush(); - } - - /** - * Get the cache driver. - * - * @return \Illuminate\Cache\CacheManager - */ - protected function getCacheDriver() - { - return app('cache')->driver($this->cacheDriver); - } - - /** - * Get the cache object with tags assigned, if applicable. - * - * @return \Illuminate\Cache\CacheManager - */ - protected function getCache() - { - $cache = $this->getCacheDriver(); - - return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache; - } - - /** - * Indicate that the query results should be cached. - * - * @param \DateTime|int $time - * @return \Rennokki\QueryCache\Query\Builder - */ - public function cacheFor($time) - { - $this->cacheTime = $time; - $this->avoidCache = false; - - return $this; - } - - /** - * Indicate that the query results should be cached forever. - * - * @return \Illuminate\Database\Query\Builder|static - */ - public function cacheForever() - { - return $this->cacheFor(-1); - } - - /** - * Indicate that the query should not be cached. - * - * @return \Illuminate\Database\Query\Builder|static - */ - public function dontCache() - { - $this->avoidCache = true; - - return $this; - } - - /** - * Alias for dontCache(). - * - * @return \Illuminate\Database\Query\Builder|static - */ - public function doNotCache() - { - return $this->dontCache(); - } - - /** - * Set the cache prefix. - * - * @param string $prefix - * @return \Rennokki\QueryCache\Query\Builder - */ - public function cachePrefix(string $prefix) - { - $this->cachePrefix = $prefix; - - return $this; - } - - /** - * Attach tags to the cache. - * - * @param array $cacheTags - * @return \Rennokki\QueryCache\Query\Builder - */ - public function cacheTags(array $cacheTags = []) - { - $this->cacheTags = $cacheTags; - - return $this; - } - - /** - * Use a specific cache driver. - * - * @param string $cacheDriver - * @return \Rennokki\QueryCache\Query\Builder - */ - public function cacheDriver(string $cacheDriver) - { - $this->cacheDriver = $cacheDriver; - - return $this; - } - - /** - * Use a plain key instead of a hashed one in the cache driver. - * - * @return \Rennokki\QueryCache\Query\Builder - */ - public function withPlainKey() - { - $this->cacheUsePlainKey = true; - - return $this; - } } diff --git a/src/Traits/QueryCacheModule.php b/src/Traits/QueryCacheModule.php new file mode 100644 index 0000000..b2a12a4 --- /dev/null +++ b/src/Traits/QueryCacheModule.php @@ -0,0 +1,356 @@ +columns)) { + $this->columns = $columns; + } + + $key = $this->getCacheKey('get'); + $cache = $this->getCache(); + $callback = $this->getQueryCacheCallback($method, $columns); + $time = $this->getCacheTime(); + + if ($time instanceof DateTime || $time > 0) { + return $cache->remember($key, $time, $callback); + } + + return $cache->rememberForever($key, $callback); + } + + /** + * Get the query cache callback. + * + * @param string $method + * @param array $columns + * @return \Closure + */ + public function getQueryCacheCallback(string $method = 'get', $columns = ['*']) + { + return function () use ($method, $columns) { + $this->avoidCache = true; + + return $this->{$method}($columns); + }; + } + + /** + * Get a unique cache key for the complete query. + * + * @param string $method + * @param string|null $id + * @param string|null $appends + * @return string + */ + public function getCacheKey(string $method = 'get', $id = null, $appends = null): string + { + $key = $this->generateCacheKey($method, $id, $appends); + $prefix = $this->getCachePrefix(); + + return "{$prefix}:{$key}"; + } + + /** + * Generate the unique cache key for the query. + * + * @param string $method + * @param string|null $id + * @param string|null $appends + * @return string + */ + public function generateCacheKey(string $method = 'get', $id = null, $appends = null): string + { + $key = $this->generatePlainCacheKey($method, $id, $appends); + + if ($this->shouldUsePlainKey()) { + return $key; + } + + return hash('sha256', $key); + } + + /** + * Generate the plain unique cache key for the query. + * + * @param string $method + * @param string|null $id + * @param string|null $appends + * @return string + */ + public function generatePlainCacheKey(string $method = 'get', $id = null, $appends = null): string + { + $name = $this->connection->getName(); + + // Count has no Sql, that's why it can't be used ->toSql() + if ($method === 'count') { + return $name.$method.$id.serialize($this->getBindings()).$appends; + } + + return $name.$method.$id.$this->toSql().serialize($this->getBindings()).$appends; + } + + /** + * Flush the cache that contains specific tags. + * + * @param array $tags + * @return bool + */ + public function flushQueryCache(array $tags = []): bool + { + $cache = $this->getCacheDriver(); + + if (! method_exists($cache, 'tags')) { + return false; + } + + foreach ($tags as $tag) { + self::flushQueryCacheWithTag($tag); + } + + return true; + } + + /** + * Flush the cache for a specific tag. + * + * @param string $tag + * @return bool + */ + public function flushQueryCacheWithTag(string $tag): bool + { + $cache = $this->getCacheDriver(); + + if (! method_exists($cache, 'tags')) { + return false; + } + + return $cache->tags($tag)->flush(); + } + + /** + * Indicate that the query results should be cached. + * + * @param \DateTime|int $time + * @return \Rennokki\QueryCache\Query\Builder + */ + public function cacheFor($time) + { + $this->cacheTime = $time; + $this->avoidCache = false; + + return $this; + } + + /** + * Indicate that the query results should be cached forever. + * + * @return \Illuminate\Database\Query\Builder|static + */ + public function cacheForever() + { + return $this->cacheFor(-1); + } + + /** + * Indicate that the query should not be cached. + * + * @return \Illuminate\Database\Query\Builder|static + */ + public function dontCache() + { + $this->avoidCache = true; + + return $this; + } + + /** + * Alias for dontCache(). + * + * @return \Illuminate\Database\Query\Builder|static + */ + public function doNotCache() + { + return $this->dontCache(); + } + + /** + * Set the cache prefix. + * + * @param string $prefix + * @return \Rennokki\QueryCache\Query\Builder + */ + public function cachePrefix(string $prefix) + { + $this->cachePrefix = $prefix; + + return $this; + } + + /** + * Attach tags to the cache. + * + * @param array $cacheTags + * @return \Rennokki\QueryCache\Query\Builder + */ + public function cacheTags(array $cacheTags = []) + { + $this->cacheTags = $cacheTags; + + return $this; + } + + /** + * Use a specific cache driver. + * + * @param string $cacheDriver + * @return \Rennokki\QueryCache\Query\Builder + */ + public function cacheDriver(string $cacheDriver) + { + $this->cacheDriver = $cacheDriver; + + return $this; + } + + /** + * Use a plain key instead of a hashed one in the cache driver. + * + * @return \Rennokki\QueryCache\Query\Builder + */ + public function withPlainKey() + { + $this->cacheUsePlainKey = true; + + return $this; + } + + /** + * Get the cache driver. + * + * @return \Illuminate\Cache\CacheManager + */ + public function getCacheDriver() + { + return app('cache')->driver($this->cacheDriver); + } + + /** + * Get the cache object with tags assigned, if applicable. + * + * @return \Illuminate\Cache\CacheManager + */ + public function getCache() + { + $cache = $this->getCacheDriver(); + $tags = $this->getCacheTags(); + + return $tags ? $cache->tags($tags) : $cache; + } + + /** + * Check if the cache operation should be avoided. + * + * @return bool + */ + public function shouldAvoidCache(): bool + { + return $this->avoidCache; + } + + /** + * Check if the cache operation key should use a plain + * query key. + * + * @return bool + */ + public function shouldUsePlainKey(): bool + { + return $this->cacheUsePlainKey; + } + + /** + * Get the cache time attribute. + * + * @return int|\DateTime + */ + public function getCacheTime() + { + return $this->cacheTime; + } + + /** + * Get the cache tags attribute. + * + * @return array|null + */ + public function getCacheTags() + { + return $this->cacheTags; + } + + /** + * Get the cache prefix attribute. + * + * @return string + */ + public function getCachePrefix(): string + { + return $this->cachePrefix; + } +} From 544a2681bb2973f137670f867208dcfe17b5613f Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sat, 14 Dec 2019 20:27:08 +0200 Subject: [PATCH 2/4] Fixed tests. --- src/Query/Builder.php | 2 +- src/Traits/QueryCacheModule.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 793fdb9..7630ac9 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -4,7 +4,7 @@ use DateTime; use Illuminate\Database\Query\Builder as BaseBuilder; -use Rennokki\QueryCache\Contract\QueryCacheModuleInterface; +use Rennokki\QueryCache\Contracts\QueryCacheModuleInterface; use Rennokki\QueryCache\Traits\QueryCacheModule; class Builder extends BaseBuilder implements QueryCacheModuleInterface diff --git a/src/Traits/QueryCacheModule.php b/src/Traits/QueryCacheModule.php index b2a12a4..856c9bf 100644 --- a/src/Traits/QueryCacheModule.php +++ b/src/Traits/QueryCacheModule.php @@ -2,13 +2,15 @@ namespace Rennokki\QueryCache\Traits; +use Carbon\Carbon; + trait QueryCacheModule { /** * The number of seconds or the DateTime instance * that specifies how long to cache the query. * - * @var int|\DateTime + * @var int|\DateTime|\Carbon\Carbon */ protected $cacheTime; @@ -67,7 +69,7 @@ public function getFromQueryCache(string $method = 'get', $columns = ['*']) $callback = $this->getQueryCacheCallback($method, $columns); $time = $this->getCacheTime(); - if ($time instanceof DateTime || $time > 0) { + if ($time instanceof DateTime || $time instanceof Carbon || $time > 0) { return $cache->remember($key, $time, $callback); } @@ -186,7 +188,7 @@ public function flushQueryCacheWithTag(string $tag): bool /** * Indicate that the query results should be cached. * - * @param \DateTime|int $time + * @param \DateTime|\Carbon\Carbon|int $time * @return \Rennokki\QueryCache\Query\Builder */ public function cacheFor($time) @@ -327,7 +329,7 @@ public function shouldUsePlainKey(): bool /** * Get the cache time attribute. * - * @return int|\DateTime + * @return int|\DateTime|\Carbon\Carbon */ public function getCacheTime() { From 21d1321dbe7455f52ed5b222d616d236f5f630a6 Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sat, 14 Dec 2019 20:27:12 +0200 Subject: [PATCH 3/4] Added readme. --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/README.md b/README.md index 7962a78..12b2c66 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,115 @@ class Book extends Model public $cacheDriver = 'dynamodb'; // equivalent of ->cacheDriver('dynamodb'); } ``` + +## Implement the caching method to your own Builder class +Since this package modifies the `newBaseQueryBuilder()` in the model, having multiple traits that +modify this function will lead to an overlap. + +This can happen in case you are creating your own Builder class for another database drivers or simply to ease out your app query builder for more flexibility. + +To solve this, all you have to do is to add the `\Rennokki\QueryCache\Traits\QueryCacheModule` trait and the `\Rennokki\QueryCache\Contracts\QueryCacheModuleInterface` interface to your `Builder` class. Make sure that the model will no longer use the original `QueryCacheable` trait. + +```php +use Rennokki\QueryCache\Traits\QueryCacheModule; +use Illuminate\Database\Query\Builder as BaseBuilder; // the base laravel builder +use Rennokki\QueryCache\Contract\QueryCacheModuleInterface; + +// MyCustomBuilder.php +class MyCustomBuilder implements QueryCacheModuleInterface +{ + use QueryCacheModule; + + // the rest of the logic here. +} + +// MyBuilderTrait.php +trait MyBuilderTrait +{ + protected function newBaseQueryBuilder() + { + return new MyCustomBuilder( + // + ); + } +} + +// app/CustomModel.php +class CustomModel extends Model +{ + use MyBuilderTrait; +} + +CustomModel::cacheFor(30)->customGetMethod(); +``` + +## Generating your own key +This is how the default key generation function looks like: +```php +public function generatePlainCacheKey(string $method = 'get', $id = null, $appends = null): string +{ + $name = $this->connection->getName(); + + // Count has no Sql, that's why it can't be used ->toSql() + if ($method === 'count') { + return $name.$method.$id.serialize($this->getBindings()).$appends; + } + + return $name.$method.$id.$this->toSql().serialize($this->getBindings()).$appends; +} +``` + +In some cases, like implementing your own Builder for MongoDB for example, you might not want to use the `toSql()` and use your own +method of generating per-sql key. You can do so by overwriting the `MyCustomBuilder` class `generatePlainCacheKey()` with your own one. + +It is, however, highly recommended to use the most of the variables provided by the function to avoid cache overlapping issues. + +```php +class MyCustomBuilder implements QueryCacheModuleInterface +{ + use QueryCacheModule; + + public function generatePlainCacheKey(string $method = 'get', $id = null, $appends = null): string + { + $name = $this->connection->getName(); + + // Using ->myCustomSqlString() instead of ->toSql() + return $name.$method.$id.$this->myCustomSqlString().serialize($this->getBindings()).$appends; + } +} +``` + +## Implementing cache for other functions than get() +Since all of the Laravel Eloquent functions are based on it, the builder that comes with this package replaces only the `get()` one: +```php +class Builder +{ + public function get($columns = ['*']) + { + if (! $this->shouldAvoidCache()) { + return $this->getFromQueryCache('get', $columns); + } + + return parent::get($columns); + } +} +``` + +In case that you want to cache your own methods from your custom builder or, for instance, your `count()` method doesn't rely on `get()`, you can replace it using this syntax: +```php +class MyCustomBuilder +{ + public function count() + { + if (! $this->shouldAvoidCache()) { + return $this->getFromQueryCache('count'); + } + + return parent::count(); + } +} +``` + +In fact, you can also replace any eloquent method within your builder if you use `$this->shouldAvoidCache()` check and retrieve the cached data using `getFromQueryCache()` method, passing the method name as string, and, optionally, an array of columns that defaults to `['*']`. + +Notice that the `getFromQueryCache()` method accepts a method name and a `$columns` parameter. If your method doesn't implement the `$columns`, don't pass it. From 3daf6a994d03991393c3e94bc70636b2b0c97cdf Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sat, 14 Dec 2019 20:29:38 +0200 Subject: [PATCH 4/4] Fixed DateTime issues. --- src/Query/Builder.php | 1 - src/Traits/QueryCacheModule.php | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 7630ac9..6e85241 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -2,7 +2,6 @@ namespace Rennokki\QueryCache\Query; -use DateTime; use Illuminate\Database\Query\Builder as BaseBuilder; use Rennokki\QueryCache\Contracts\QueryCacheModuleInterface; use Rennokki\QueryCache\Traits\QueryCacheModule; diff --git a/src/Traits/QueryCacheModule.php b/src/Traits/QueryCacheModule.php index 856c9bf..7511996 100644 --- a/src/Traits/QueryCacheModule.php +++ b/src/Traits/QueryCacheModule.php @@ -2,7 +2,7 @@ namespace Rennokki\QueryCache\Traits; -use Carbon\Carbon; +use DateTime; trait QueryCacheModule { @@ -10,7 +10,7 @@ trait QueryCacheModule * The number of seconds or the DateTime instance * that specifies how long to cache the query. * - * @var int|\DateTime|\Carbon\Carbon + * @var int|\DateTime */ protected $cacheTime; @@ -69,7 +69,7 @@ public function getFromQueryCache(string $method = 'get', $columns = ['*']) $callback = $this->getQueryCacheCallback($method, $columns); $time = $this->getCacheTime(); - if ($time instanceof DateTime || $time instanceof Carbon || $time > 0) { + if ($time instanceof DateTime || $time > 0) { return $cache->remember($key, $time, $callback); } @@ -188,7 +188,7 @@ public function flushQueryCacheWithTag(string $tag): bool /** * Indicate that the query results should be cached. * - * @param \DateTime|\Carbon\Carbon|int $time + * @param \DateTime|int $time * @return \Rennokki\QueryCache\Query\Builder */ public function cacheFor($time) @@ -329,7 +329,7 @@ public function shouldUsePlainKey(): bool /** * Get the cache time attribute. * - * @return int|\DateTime|\Carbon\Carbon + * @return int|\DateTime */ public function getCacheTime() {