diff --git a/composer.json b/composer.json index a000c851..ba03244e 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "illuminate/http": "^9.0 || ^10.0", "illuminate/routing": "^9.0 || ^10.0", "illuminate/support": "^9.0 || ^10.0", + "laravel/serializable-closure": "^1.3", "livewire/livewire": "dev-main", "nesbot/carbon": "^2.67" }, diff --git a/src/Concerns/SerializesClosures.php b/src/Concerns/SerializesClosures.php new file mode 100644 index 00000000..a23a3e30 --- /dev/null +++ b/src/Concerns/SerializesClosures.php @@ -0,0 +1,33 @@ + + */ + public function __serialize(): array + { + return array_map(function ($value) { + return $value instanceof Closure ? new SerializableClosure($value) : $value; + }, get_object_vars($this)); + } + + /** + * Restore the instance values after serialization. + * + * @param array $data + */ + public function __unserialize(array $data): void + { + foreach ($data as $key => $value) { + $this->{$key} = $value instanceof SerializableClosure ? $value->getClosure() : $value; + } + } +} diff --git a/src/Entry.php b/src/Entry.php index f2cc791e..1a755379 100644 --- a/src/Entry.php +++ b/src/Entry.php @@ -15,7 +15,7 @@ public function __construct(public string $table, public array $attributes) } /** - * Resolve the entry for ingest and storage. + * Resolve the entry for ingest. */ public function resolve(): self { diff --git a/src/Update.php b/src/Update.php index d02e2eef..2eb68550 100644 --- a/src/Update.php +++ b/src/Update.php @@ -3,9 +3,12 @@ namespace Laravel\Pulse; use Closure; +use Laravel\Pulse\Concerns\SerializesClosures; class Update { + use SerializesClosures; + /** * @param array $conditions * @param array|(\Closure(array): array) $attributes @@ -19,7 +22,7 @@ public function __construct( } /** - * Resolve the update for ingest and storage. + * Resolve the update for ingest. */ public function resolve(): self { diff --git a/tests/Feature/RedisIngestTest.php b/tests/Feature/RedisIngestTest.php new file mode 100644 index 00000000..42934b3e --- /dev/null +++ b/tests/Feature/RedisIngestTest.php @@ -0,0 +1,139 @@ + 'unique-job-id', + ], function () { + return [ + 'slowest' => 66, + ]; + }); + + $pulse->record($update); + $pulse->store($ingest); + $ingest->store($storage); + + expect($storage->stored)->toHaveCount(1); + expect($storage->stored[0])->toBeInstanceOf(Update::class); + expect($storage->stored[0])->not->toBe($update); + expect($storage->stored[0]->conditions)->toBe([ + 'job_uuid' => 'unique-job-id', + ]); + expect(($storage->stored[0]->attributes)())->toBe([ + 'slowest' => 66, + ]); +}); + +it('can ingest an update with a closure using external scope', function () { + $pulse = App::make(Pulse::class); + $ingest = App::make(Redis::class); + $storage = new StorageFake; + $duration = 66; + $update = new Update('pulse_jobs', [ + 'job_uuid' => 'unique-job-id', + ], function () use ($duration) { + return [ + 'slowest' => $duration, + ]; + }); + + $pulse->record($update); + $pulse->store($ingest); + $ingest->store($storage); + + expect($storage->stored)->toHaveCount(1); + expect($storage->stored[0])->toBeInstanceOf(Update::class); + expect($storage->stored[0])->not->toBe($update); + expect($storage->stored[0]->conditions)->toBe([ + 'job_uuid' => 'unique-job-id', + ]); + expect(($storage->stored[0]->attributes)())->toBe([ + 'slowest' => 66, + ]); +}); + +it('can ingest an update with a short closure', function () { + $pulse = App::make(Pulse::class); + $ingest = App::make(Redis::class); + $storage = new StorageFake; + $update = new Update('pulse_jobs', [ + 'job_uuid' => 'unique-job-id', + ], fn () => [ + 'slowest' => 66, + ]); + + $pulse->record($update); + $pulse->store($ingest); + $ingest->store($storage); + + expect($storage->stored)->toHaveCount(1); + expect($storage->stored[0])->toBeInstanceOf(Update::class); + expect($storage->stored[0])->not->toBe($update); + expect($storage->stored[0]->conditions)->toBe([ + 'job_uuid' => 'unique-job-id', + ]); + expect(($storage->stored[0]->attributes)())->toBe([ + 'slowest' => 66, + ]); +}); + +it('can ingest an update with a short closure using external scope', function () { + $pulse = App::make(Pulse::class); + $ingest = App::make(Redis::class); + $storage = new StorageFake; + $duration = 66; + $update = new Update('pulse_jobs', [ + 'job_uuid' => 'unique-job-id', + ], fn () => [ + 'slowest' => $duration, + ]); + + $pulse->record($update); + $pulse->store($ingest); + $ingest->store($storage); + + expect($storage->stored)->toHaveCount(1); + expect($storage->stored[0])->toBeInstanceOf(Update::class); + expect($storage->stored[0])->not->toBe($update); + expect($storage->stored[0]->conditions)->toBe([ + 'job_uuid' => 'unique-job-id', + ]); + expect(($storage->stored[0]->attributes)())->toBe([ + 'slowest' => 66, + ]); +}); + +it('can ingest an update array based attributes', function () { + $pulse = App::make(Pulse::class); + $ingest = App::make(Redis::class); + $storage = new StorageFake; + $duration = 66; + $update = new Update('pulse_jobs', [ + 'job_uuid' => 'unique-job-id', + ], [ + 'slowest' => $duration, + ]); + + $pulse->record($update); + $pulse->store($ingest); + $ingest->store($storage); + + expect($storage->stored)->toHaveCount(1); + expect($storage->stored[0])->toBeInstanceOf(Update::class); + expect($storage->stored[0])->not->toBe($update); + expect($storage->stored[0]->conditions)->toBe([ + 'job_uuid' => 'unique-job-id', + ]); + expect($storage->stored[0]->attributes)->toBe([ + 'slowest' => 66, + ]); +}); diff --git a/tests/StorageFake.php b/tests/StorageFake.php new file mode 100644 index 00000000..04af79cb --- /dev/null +++ b/tests/StorageFake.php @@ -0,0 +1,37 @@ + $items + */ + public function store(Collection $items): void + { + $this->stored = $this->stored->merge($items); + } + + /** + * Trim the stored entries from the given tables. + * + * @param \Illuminate\Support\Collection $tables + */ + public function trim(Collection $tables): void + { + // + } +}