ldc-arcadia-genesis-php-client/src/Lib/Pagination/StreamOf.php

163 lines
3.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Uwil\ArcadiaApiPhpClient\Lib\Pagination;
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PaginationDto;
/**
* Lazy stream / cursor iterator over paginated API resources.
*
* Provides continuous O(1) memory iteration across all pages.
*
* @template T
*
* @implements \IteratorAggregate<int, T>
*/
final class StreamOf implements \IteratorAggregate
{
private ?ApiError $lastError = null;
private ?PaginationDto $firstPagePagination = null;
/**
* @param \Closure(Pagination):(\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf<T>|ApiError) $pageFetcher
*/
public function __construct(
private readonly \Closure $pageFetcher,
private readonly PageSize $pageSize = new PageSize(),
) {
}
/**
* Iterate over individual items of type T.
*
* @return \Generator<int, T, mixed, ?ApiError>
*/
public function getIterator(): \Generator
{
foreach ($this->pages() as $page) {
foreach ($page->items() as $item) {
yield $item;
}
}
return $this->lastError;
}
/**
* Iterate page by page (useful for batching operations).
*
* @return \Generator<int,\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf<T>, mixed, ?ApiError>
*/
public function pages(): \Generator
{
$this->lastError = null;
$pageNumber = 1;
do {
$pagination = Pagination::ofPage(new PageNumber($pageNumber), $this->pageSize);
$result = ($this->pageFetcher)($pagination);
if ($result instanceof ApiError) {
$this->lastError = $result;
return $result;
}
if ($this->firstPagePagination === null) {
$this->firstPagePagination = $result->pagination();
}
yield $result;
if (! $result->pagination()->hasNextPage()) {
break;
}
$pageNumber++;
} while (true);
return null;
}
/**
* Execute a callback on each item.
* Stops and returns the ApiError if an error occurs, or stops if the callback returns false.
*
* @param callable(T): (bool|void) $callback
*/
public function each(callable $callback): ?ApiError
{
foreach ($this->getIterator() as $item) {
if ($callback($item) === false) {
break;
}
}
return $this->lastError;
}
/**
* Yield at most $limit items without requesting subsequent pages unnecessarily.
*
* @return \Generator<int, T>
*/
public function take(int $limit): \Generator
{
if ($limit <= 0) {
return;
}
$count = 0;
foreach ($this->getIterator() as $item) {
yield $item;
$count++;
if ($count >= $limit) {
break;
}
}
}
/**
* Get the first item of the stream, or null if empty, or ApiError on failure.
*
* @return T|ApiError|null
*/
public function first(): mixed
{
foreach ($this->take(1) as $item) {
return $item;
}
return $this->lastError;
}
public function lastError(): ?ApiError
{
return $this->lastError;
}
public function hasError(): bool
{
return $this->lastError !== null;
}
/**
* Total number of items reported by the API (available once at least one page is fetched).
*/
public function totalCount(): ?int
{
return $this->firstPagePagination?->totalItems;
}
/**
* Total number of pages reported by the API (available once at least one page is fetched).
*/
public function totalPages(): ?int
{
return $this->firstPagePagination?->totalPages;
}
}