Skip to content

Commit

Permalink
Merge pull request #7 from vormkracht10/feature/spatie-once
Browse files Browse the repository at this point in the history
Feature/spatie once
  • Loading branch information
david-d-h authored Feb 8, 2024
2 parents 3571d80 + a26bf29 commit 68aad49
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 47 deletions.
75 changes: 64 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -34,8 +35,7 @@
},
"autoload": {
"psr-4": {
"Vormkracht10\\PermanentCache\\": "src/",
"Vormkracht10\\PermanentCache\\Database\\Factories\\": "database/factories/"
"Vormkracht10\\PermanentCache\\": "src/"
}
},
"autoload-dev": {
Expand Down
107 changes: 75 additions & 32 deletions src/Cached.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

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;

/**
* @method mixed run()
*
* @template E
* @template V
*/
abstract class Cached
{
Expand All @@ -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<E>|null
* @var class-string|array<int, class-string>|null
*/
protected $event = null;

Expand All @@ -46,53 +47,75 @@ 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 */
$this->run($event),
);
/** @phpstan-ignore-next-line */
if (null === $update = $this->run($event)) {
return;
}

Cache::driver($driver)->forever($ident, $update);
}

/**
* 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)) {
Expand All @@ -113,20 +136,22 @@ public static function schedule($callback)
/**
* Get the event (if any) this cacher listens for.
*
* @return array<int, class-string<E>>
* @return array<int, class-string>
*/
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());
});
}

/**
Expand All @@ -142,4 +167,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'),
);
});
}
}
3 changes: 2 additions & 1 deletion src/PermanentCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ class PermanentCache
*/
public function caches(array $cachers): self
{
/** @var class-string<Cached> $cacher */
foreach ($cachers as $cacher) {
$events = $cacher::getListenerEvent();
$events = $cacher::getListenerEvents();

$resolved[$cacher] = $events;

Expand Down

0 comments on commit 68aad49

Please sign in to comment.