163 lines
3.9 KiB
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;
|
|
}
|
|
}
|