From ce461fbacd39e97ae2362f0915f094903c767ce7 Mon Sep 17 00:00:00 2001 From: David <75451291+dulkoss@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:52:36 +0100 Subject: [PATCH 1/4] make use of spatie/once to reduce the amount of work done by the Cached class. --- composer.json | 6 +-- src/Cached.php | 97 ++++++++++++++++++++++++++++++------------ src/PermanentCache.php | 3 +- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 89a5527..aea7373 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,9 @@ ], "require": { "php": "^8.1", + "illuminate/contracts": "^10.0", "spatie/laravel-package-tools": "^1.14.0", - "illuminate/contracts": "^10.0" + "spatie/once": "^3.1" }, "require-dev": { "laravel/pint": "^1.0", @@ -34,8 +35,7 @@ }, "autoload": { "psr-4": { - "Vormkracht10\\PermanentCache\\": "src/", - "Vormkracht10\\PermanentCache\\Database\\Factories\\": "database/factories/" + "Vormkracht10\\PermanentCache\\": "src/" } }, "autoload-dev": { diff --git a/src/Cached.php b/src/Cached.php index a272ec7..cb7a58b 100644 --- a/src/Cached.php +++ b/src/Cached.php @@ -4,6 +4,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use ReflectionClass; @@ -11,7 +12,7 @@ /** * @method mixed run() * - * @template E + * @template V */ abstract class Cached { @@ -30,7 +31,7 @@ abstract class Cached * as it can also be inferred by type hinting an argument * in the run method. * - * @var class-string|null + * @var class-string|array|null */ protected $event = null; @@ -46,13 +47,11 @@ abstract class Cached * Update the cached value, this method expects an event if * the cacher is not static. * - * @param E $event + * @internal You shouldn't call this yourself. */ final public function handle($event = null): void { - [$driver, $ident] = self::parseCacheString($this->store - ?? throw new \Exception('The $store property in ['.static::class.'] must be overridden'), - ); + [$driver, $ident] = self::store(); Cache::driver($driver)->forever($ident, /** @phpstan-ignore-next-line */ @@ -63,36 +62,58 @@ final public function handle($event = null): void /** * Manually force a static cache to update. */ - final public static function update(): void + final public static function update(): ?PendingDispatch { $instance = app()->make(static::class); if (! is_a(static::class, ShouldQueue::class, true)) { $instance->handle(); - return; + return null; } - dispatch($instance); + return dispatch($instance); } /** * Get the cached value this cacher provides. + * + * @param bool $update Whether the cache should update + * when it doesn't hold the value yet. + * @return V|mixed|null */ - final public static function get(): mixed + final public static function get($default = null, bool $update = false): mixed { - $store = (new ReflectionClass(static::class)) - ->getProperty('store') - ->getDefaultValue(); + [$driver, $ident] = self::store(); - [$driver, $ident] = self::parseCacheString($store - ?? throw new \Exception('The $store property in ['.static::class.'] must be overridden'), - ); + $cache = Cache::driver($driver); + + if ($update && ! $cache->has($ident)) { + static::update()?->onConnection('sync'); + } + + return $cache->get($ident, $default); + } + + /** + * Get the cached value this cacher provides. + * + * This method should be used inside your cachers + * instead of the static `static::get` method to prevent + * infinite recursion. + * + * @return V|mixed|null + */ + final protected function value($default = null): mixed + { + [$driver, $ident] = self::store(); - return Cache::driver($driver)->get($ident); + return Cache::driver($driver)->get( + $ident, $default, + ); } - // Default implementation for the \Scheduled::schedule method. + /// Default implementation for the `\Scheduled::schedule` method. public static function schedule($callback) { if (! is_a(static::class, Scheduled::class, true)) { @@ -113,20 +134,22 @@ public static function schedule($callback) /** * Get the event (if any) this cacher listens for. * - * @return array> + * @return array */ - final public static function getListenerEvent(): array + final public static function getListenerEvents(): array { - $reflection = new ReflectionClass(static::class); + return once(function () { + $reflection = new ReflectionClass(static::class); - $concrete = Arr::wrap($reflection->getProperty('event')->getDefaultValue()); + $concrete = Arr::wrap($reflection->getProperty('event')->getDefaultValue()); - /** @phpstan-ignore-next-line */ - return $concrete ?: Arr::wrap(($reflection - ->getMethod('run') - ->getParameters()[0] ?? null) - ?->getType() - ?->getName()); + /** @phpstan-ignore-next-line */ + return $concrete ?: Arr::wrap(($reflection + ->getMethod('run') + ->getParameters()[0] ?? null) + ?->getType() + ?->getName()); + }); } /** @@ -142,4 +165,22 @@ private static function parseCacheString(string $store): array return [$driver, $ident]; } + + /** + * Get the driver and identifier specified in the $store property. + * + * @return array{string, string} + */ + private static function store(): array + { + return once(function () { + $store = (new ReflectionClass(static::class)) + ->getProperty('store') + ->getDefaultValue(); + + return self::parseCacheString($store + ?? throw new \Exception('The $store property in ['.static::class.'] must be overridden'), + ); + }); + } } diff --git a/src/PermanentCache.php b/src/PermanentCache.php index 8712b71..84c35f6 100755 --- a/src/PermanentCache.php +++ b/src/PermanentCache.php @@ -17,8 +17,9 @@ class PermanentCache */ public function caches(array $cachers): self { + /** @var class-string $cacher */ foreach ($cachers as $cacher) { - $events = $cacher::getListenerEvent(); + $events = $cacher::getListenerEvents(); $resolved[$cacher] = $events; From 10a8ecb8655c805724aba9d28419c8950a6c05c7 Mon Sep 17 00:00:00 2001 From: David <75451291+dulkoss@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:17:52 +0100 Subject: [PATCH 2/4] update documentation --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b953349..797f761 100644 --- a/README.md +++ b/README.md @@ -47,24 +47,75 @@ class HelloCache extends Cached } ``` +##### if you don't want to type hint the `TestEvent` class in the `run` method, you can also explicitly specify the type like so `protected $event = TestEvent::class;` + To know *where* to cache the returned value, we have the `$store` property. This is formatted like `driver:identifier`, but you can also omit the `driver:` like so `protected $store = 'hello';` and we will use the config's `cache.default` value instead. -##### if you don't want to type hint the `TestEvent` class in the `run` method, you can also explicitly specify the type like so `protected $event = TestEvent::class;` +--- + +To get the value from a cache class, you can use the static `get` method. + +```php +$greeting = HelloCache::get(); +``` + +--- + +You can specify a default value too. + +```php +$greeting = HelloCache::get('Welcome'); +``` + +--- + +If you want the cache to update when it doesn't hold a value for +your cache yet, you can set the `$update` argument to true. + +```php +$greeting = HelloCache::get(update: true); +``` + +--- + +### Reactive caches that listen to many events + +Sometimes you may want to update a cache when one of many events occur, +for example you might want to gather some information on a page when +some content is created, deleted or updated. + +You can do that by assigning the `$event` property an array of event names. + +```php +class SomeCache extends Cached +{ + protected $event = [ + ContentCreated::class, + ContentDeleted::class, + ContentUpdated::class, + ]; + + // You have access to an $event object here, + // but be careful! This event object may be any of + // the events listed above. + public function run($event) + { + // do something... + } +} +``` ## "Static" caches Static caches are a little different to the Reactive caches, these do not respond to events -and must be called manually or scheduled. Here is an example. - -By default, a cache will not do anything if it doesn't listen for any events. -Thus, we need to schedule it. +and must be updated manually or scheduled. Here is an example. ### Scheduling with cron expressions You can use cron expressions to schedule your cache, a very basic example is shown below. -This will "run" the cache every minute. +This will run the cache every minute. ```php use Vormkracht10\PermanentCache\Scheduled; @@ -77,16 +128,18 @@ class MinutesCache extends Cached implements Scheduled protected $expression = '* * * * *'; - public function run(): mixed + public function run(): int { - return CounterCache::get() + 1; + return $this->value() + 1; } } ``` +##### Warning: you should not use Cache::get() in the cache itself, use $this->value() instead, this is to prevent infinite recursion from happening. + ### Static caches with Laravel magic -Now you can run `php artisan schedule:work` and every minute, the `minutes` count will be incremented! +Now you can run `php artisan schedule:work` and every minute, the `minutes` count will be incremented. But, if you're anything like me, you don't really like writing raw cron expressions and much rather use Laravel's cool `Schedule` class. Well, you can. @@ -99,7 +152,7 @@ class MinutesCache extends Cached implements Scheduled public function run(): mixed { - return CounterCache::get() + 1; + return $this->value() + 1; } public static function schedule($callback) @@ -139,7 +192,7 @@ class HelloCache extends Cached implements ShouldQueue You can specify a bunch of things, like the queue connection using the `$connection` property. You can basically configure you cache as a Laravel job. This works because the `Cached` class from which -we are inheriting is structured like a Laravel job! +we are inheriting is structured like a Laravel job. ##### [Read more on Jobs & Queues](https://laravel.com/docs/queues) From bb34bc2af2ea421dfe2ec1a66880caeedf85a807 Mon Sep 17 00:00:00 2001 From: David <75451291+dulkoss@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:00:20 +0100 Subject: [PATCH 3/4] don't update the cache when null return value --- src/Cached.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Cached.php b/src/Cached.php index cb7a58b..cceea2b 100644 --- a/src/Cached.php +++ b/src/Cached.php @@ -53,10 +53,12 @@ final public function handle($event = null): void { [$driver, $ident] = self::store(); - Cache::driver($driver)->forever($ident, - /** @phpstan-ignore-next-line */ - $this->run($event), - ); + /** @phpstan-ignore-next-line */ + if (null === $update = $this->run($event)) { + return; + } + + Cache::driver($driver)->forever($ident, $update); } /** From a26bf29777d3aba3120f7a8d1d146846fbc3687d Mon Sep 17 00:00:00 2001 From: daviddenhaan Date: Thu, 8 Feb 2024 11:00:45 +0000 Subject: [PATCH 4/4] Fix styling --- src/Cached.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cached.php b/src/Cached.php index cceea2b..d8f9205 100644 --- a/src/Cached.php +++ b/src/Cached.php @@ -80,8 +80,8 @@ final public static function update(): ?PendingDispatch /** * Get the cached value this cacher provides. * - * @param bool $update Whether the cache should update - * when it doesn't hold the value yet. + * @param bool $update Whether the cache should update + * when it doesn't hold the value yet. * @return V|mixed|null */ final public static function get($default = null, bool $update = false): mixed