diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 30188ff19f92..7efb0ebd1779 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -4,6 +4,8 @@ use ArrayIterator; use Closure; +use DateInterval; +use DateTimeImmutable; use DateTimeInterface; use Generator; use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString; @@ -1750,6 +1752,42 @@ public function values() }); } + /** + * Run the given callback every time the interval has passed. + * + * @return static + */ + public function withHeartbeat(DateInterval|int $interval, callable $callback) + { + $seconds = is_int($interval) ? $interval : $this->intervalSeconds($interval); + + return new static(function () use ($seconds, $callback) { + $start = $this->now(); + + foreach ($this as $key => $value) { + $now = $this->now(); + + if (($now - $start) >= $seconds) { + $callback(); + + $start = $now; + } + + yield $key => $value; + } + }); + } + + /** + * Get the total seconds from the given interval. + */ + protected function intervalSeconds(DateInterval $interval): int + { + $start = new DateTimeImmutable(); + + return $start->add($interval)->getTimestamp() - $start->getTimestamp(); + } + /** * Zip the collection together with one or more arrays. * diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index 328ca23b2af4..084a6dec0d2b 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -1706,6 +1706,21 @@ public function testWhereStrictIsLazy() }); } + public function testWithHeartbeatIsLazy() + { + $this->assertDoesNotEnumerate(function ($collection) { + $collection->withHeartbeat(1, function () { + // Heartbeat callback + }); + }); + + $this->assertEnumeratesOnce(function ($collection) { + $collection->withHeartbeat(1, function () { + // Heartbeat callback + })->all(); + }); + } + public function testWrapIsLazy() { $this->assertDoesNotEnumerate(function ($collection) { diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index 5fe161cd9025..5dcedbc824c6 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -447,4 +447,51 @@ public function testDot() $this->assertEquals($expected, $dotted->all()); } + + public function testWithHeartbeat() + { + $start = Carbon::create(2000, 1, 1); + $after2Minutes = $start->copy()->addMinutes(2); + $after5Minutes = $start->copy()->addMinutes(5); + $after7Minutes = $start->copy()->addMinutes(7); + $after11Minutes = $start->copy()->addMinutes(11); + + Carbon::setTestNow($start); + + $output = new Collection(); + + $numbers = LazyCollection::range(1, 10) + + // Move the clock to possibly trigger the heartbeat... + ->tapEach(fn ($number) => Carbon::setTestNow( + match ($number) { + 3 => $after2Minutes, + 4 => $after5Minutes, + 6 => $after7Minutes, + 9 => $after11Minutes, + default => Carbon::now(), + } + )) + + // Push the current date to `output` when heartbeat is triggered... + ->withHeartbeat(Duration::minutes(5), fn () => $output[] = Carbon::now()) + + // Push every number onto `output` as it's enumerated... + ->tapEach(fn ($number) => $output[] = $number)->all(); + + $this->assertEquals(range(1, 10), $numbers); + + $this->assertEquals( + [ + 1, 2, 3, + $after5Minutes, + 4, 5, 6, 7, 8, + $after11Minutes, + 9, 10, + ], + $output->all(), + ); + + Carbon::setTestNow(); + } }