Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions src/Exception/InvalidPaginationArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ class InvalidPaginationArgumentException extends \InvalidArgumentException imple
private array $parameters;

/**
* @param array<string, mixed> $parameters The parameter values that were provided
* @param string $message The error message
* @param int $code The error code (optional)
* @param \Throwable|null $previous The previous exception (optional)
* Create a new InvalidPaginationArgumentException containing the parameter values that caused the error.
*
* @param array<string, mixed> $parameters Associative map of parameter names to the values that triggered the exception.
* @param string $message Human-readable error message.
* @param int $code Optional error code.
* @param \Throwable|null $previous Optional previous exception for chaining.
*/
public function __construct(
array $parameters,
Expand All @@ -41,13 +43,13 @@ public function __construct(
}

/**
* Create an exception for invalid parameter values.
* Create an exception representing a single invalid pagination parameter.
*
* @param string $parameterName The name of the invalid parameter
* @param mixed $value The invalid value
* @param string $description Description of what the parameter represents
* @param string $parameterName The name of the invalid parameter.
* @param mixed $value The provided value for the parameter.
* @param string $description Short description of what the parameter represents.
*
* @return self
* @return self An exception containing the invalid parameter and a message describing the expected value.
*/
public static function forInvalidParameter(
string $parameterName,
Expand All @@ -67,13 +69,13 @@ public static function forInvalidParameter(
}

/**
* Create an exception for invalid zero limit combinations.
* Create an exception describing an invalid combination where `limit` is zero but `offset` or `nowCount` are non-zero.
*
* @param int $offset The offset value
* @param int $limit The limit value (should be 0)
* @param int $nowCount The nowCount value
* @param int $offset The pagination offset that was provided.
* @param int $limit The pagination limit value (expected to be zero in this check).
* @param int $nowCount The current count of items already paginated.
*
* @return self
* @return self An exception instance containing the keys `offset`, `limit`, and `nowCount` in its parameters.
*/
public static function forInvalidZeroLimit(int $offset, int $limit, int $nowCount): self
{
Expand All @@ -96,11 +98,11 @@ public static function forInvalidZeroLimit(int $offset, int $limit, int $nowCoun
}

/**
* Get a specific parameter value.
* Retrieve the value for a named parameter stored on the exception.
*
* @param string $name The parameter name
* @param string $name Name of the parameter to retrieve.
*
* @return mixed The parameter value, or null if not set
* @return mixed The parameter's value if set, or null if not present.
*/
public function getParameter(string $name): mixed
{
Expand Down
8 changes: 4 additions & 4 deletions src/Exception/InvalidPaginationResultException.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ public static function forInvalidCallbackResult(mixed $result, string $expectedT
}

/**
* Create an exception for invalid source result type.
* Create an exception representing a source result type mismatch.
*
* @param mixed $result The invalid result
* @param string $expectedType The expected type/class
* @param mixed $result The value returned by the source.
* @param string $expectedType The expected class or type name.
*
* @return self
* @return self An exception instance describing the expected and actual types.
*/
public static function forInvalidSourceResult(mixed $result, string $expectedType): self
{
Expand Down
94 changes: 60 additions & 34 deletions src/OffsetAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,41 @@
readonly class OffsetAdapter
{
/**
* Create an adapter with a custom source implementation.
* Initialize the adapter with the given data source.
*
* @param SourceInterface<T> $source
* Stores the provided SourceInterface implementation for fetching page-based data.
*
* @param SourceInterface<T> $source The underlying page-based data source.
*/
public function __construct(protected SourceInterface $source)
{
}

/**
* Create an adapter using a callback function.
* Create an adapter backed by a callback-based source.
*
* This is the most convenient way to use the adapter for simple cases.
* Your callback will receive (page, pageSize) and should return a Generator.
* The callback is called with ($page, $pageSize) and must return a Generator yielding items of type `T`.
*
* @param callable(int, int): \Generator<T> $callback
* @param callable(int, int): \Generator<T> $callback Callback that provides page data.
*
* @return self<T>
* @return self<T> An adapter instance that uses the provided callback as its data source.
*/
public static function fromCallback(callable $callback): self
{
return new self(new SourceCallbackAdapter($callback));
}

/**
* Execute pagination request with offset and limit.
* Execute an offset-based pagination request and return a result wrapper.
*
* @param int $offset Starting position (0-based)
* @param int $limit Maximum number of items to return
* @param int $nowCount Current count of items already fetched (used for progress tracking in multi-request scenarios)
* @param int $offset Starting position (0-based).
* @param int $limit Maximum number of items to return; zero means no limit.
* @param int $nowCount Current count of items already fetched (used for progress tracking across requests).
*
* @throws \Throwable
* @throws InvalidPaginationArgumentException If any argument is invalid (negative values or zero limit with non-zero offset/nowCount).
* @throws \Throwable For errors raised by the underlying source during data retrieval.
*
* @return OffsetResult<T>
* @return OffsetResult<T> A wrapper exposing the paginated items (via generator() and fetchAll()) respecting the provided offset and limit.
*/
public function execute(int $offset, int $limit, int $nowCount = 0): OffsetResult
{
Expand All @@ -77,45 +79,48 @@ public function execute(int $offset, int $limit, int $nowCount = 0): OffsetResul
}

/**
* Get results as a generator (advanced usage).
*
* For most use cases, use execute() instead and call fetchAll() on the result.
* Return a generator that yields paginated results for the given offset and limit.
*
* @param int $offset
* @param int $limit
* @param int $nowCount
* @param int $offset The zero-based offset of the first item to return.
* @param int $limit The maximum number of items to return; use 0 for no limit.
* @param int $nowCount The number of items already delivered prior to this call (affects internal page calculation).
*
* @throws \Throwable
* @throws \Throwable Propagates errors thrown by the underlying source.
*
* @return \Generator<T>
* @return \Generator<T> A generator that yields the resulting items.
*/
public function generator(int $offset, int $limit, int $nowCount = 0): \Generator
{
return $this->execute($offset, $limit, $nowCount)->generator();
}

/**
* Execute pagination and return all results as an array.
*
* This is a convenience method for the most common use case.
*
* @param int $offset
* @param int $limit
* @param int $nowCount
* Fetches all items for the given offset and limit and returns them as an array.
*
* @throws \Throwable
* @param int $offset The zero-based offset at which to start retrieving items.
* @param int $limit The maximum number of items to retrieve (0 means no limit).
* @param int $nowCount The number of items already delivered before this call; affects pagination calculation.
*
* @return array<T>
* @return array<T> The list of items retrieved for the requested offset and limit.
*/
public function fetchAll(int $offset, int $limit, int $nowCount = 0): array
{
return $this->execute($offset, $limit, $nowCount)->fetchAll();
}

/**
* @throws \Throwable
* Produces a sequence of per-page generators that provide items according to the offset/limit pagination request.
*
* @return \Generator<\Generator<T>>
* The returned generator yields generators (one per fetched page) that each produce items of type `T`. Pagination continues
* until the overall requested `limit` is satisfied, the underlying source signals completion, or the computed page/page size is non-positive.
*
* @param int $offset Number of items to skip before starting to collect results.
* @param int $limit Maximum number of items to return (0 means no limit).
* @param int $nowCount Current count of already-delivered items to consider when computing subsequent pages.
*
* @throws \Throwable Propagates unexpected errors from the underlying source or pagination logic.
*
* @return \Generator<\Generator<T>> A generator that yields per-page generators of items.
*/
protected function logic(int $offset, int $limit, int $nowCount): \Generator
{
Expand Down Expand Up @@ -150,6 +155,15 @@ protected function logic(int $offset, int $limit, int $nowCount): \Generator
}
}

/**
* Validate pagination arguments and throw when they are invalid.
*
* @param int $offset Starting position in the dataset.
* @param int $limit Maximum number of items to return (0 means no limit).
* @param int $nowCount Number of items already fetched prior to this request.
*
* @throws InvalidPaginationArgumentException If any parameter is negative, or if `$limit` is 0 while `$offset` or `$nowCount` is non‑zero.
*/
private function assertArgumentsAreValid(int $offset, int $limit, int $nowCount): void
{
foreach ([['offset', $offset], ['limit', $limit], ['nowCount', $nowCount]] as [$name, $value]) {
Expand All @@ -170,7 +184,14 @@ private function assertArgumentsAreValid(int $offset, int $limit, int $nowCount)
}

/**
* Create a generator that respects the overall limit.
* Yields items from the provided source generator while enforcing an overall limit.
*
* @param \Generator $sourceGenerator Generator producing source items.
* @param int $limit Overall maximum number of items to yield; 0 means no limit.
* @param int &$totalDelivered Reference to a counter incremented for each yielded item.
* @param int &$currentNowCount Reference to the current "now" count incremented for each yielded item.
*
* @return \Generator Yields items from `$sourceGenerator` until `$limit` is reached or the source is exhausted; updates `$totalDelivered` and `$currentNowCount`.
*/
private function createLimitedGenerator(
\Generator $sourceGenerator,
Expand All @@ -190,7 +211,12 @@ private function createLimitedGenerator(
}

/**
* Determine if pagination should continue.
* Decides whether pagination should continue based on the requested limit and items already delivered.
*
* @param int $limit The overall requested maximum number of items; zero indicates no limit.
* @param int $delivered The number of items delivered so far.
*
* @return bool `true` if pagination should continue (when `$limit` is zero or `$delivered` is less than `$limit`), `false` otherwise.
*/
private function shouldContinuePagination(int $limit, int $delivered): bool
{
Expand Down
42 changes: 32 additions & 10 deletions src/OffsetResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ class OffsetResult
private \Generator $generator;

/**
* @param \Generator<\Generator<T>> $sourceResultGenerator
* Create an OffsetResult from a generator that yields page generators.
*
* The provided generator must yield per-page generators whose values are items of type T; the constructor stores an internal generator that will iterate items across all pages in sequence. The internal generator can be consumed only once.
*
* @param \Generator<\Generator<T>> $sourceResultGenerator Generator that yields per-page generators of items of type T.
*/
public function __construct(\Generator $sourceResultGenerator)
{
$this->generator = $this->execute($sourceResultGenerator);
}

/**
* @return OffsetResult<T>
* Create an OffsetResult that yields no items.
*
* @return OffsetResult<T> An OffsetResult containing zero elements.
*/
public static function empty(): self
{
Expand All @@ -49,7 +55,11 @@ public static function empty(): self
}

/**
* @return T|null
* Retrieve the next item from the internal generator.
*
* The internal generator is advanced so subsequent calls return the following items.
*
* @return T|null The next yielded value, or `null` if there are no more items.
*/
public function fetch(): mixed
{
Expand All @@ -64,7 +74,11 @@ public function fetch(): mixed
}

/**
* @return array<T>
* Retrieve all remaining items from the internal generator as an array.
*
* Consuming the returned items advances the internal generator until it is exhausted.
*
* @return array<T> An array containing every remaining yielded item; empty if none remain.
*/
public function fetchAll(): array
{
Expand All @@ -79,27 +93,35 @@ public function fetchAll(): array
}

/**
* Returns the internal generator for advanced use cases.
* Get the internal generator used to stream paginated items.
*
* Warning: The generator can only be consumed once. After calling
* fetch(), fetchAll(), or iterating this generator, it will be exhausted.
* The returned generator can be consumed only once; calling fetch(), fetchAll(), or iterating the generator will exhaust it.
*
* @return \Generator<T>
* @return \Generator<T> The internal generator that yields items of type T.
*/
public function generator(): \Generator
{
return $this->generator;
}

/**
* Number of items fetched so far.
*
* @return int The count of items that have been retrieved from the internal generator.
*/
public function getFetchedCount(): int
{
return $this->fetchedCount;
}

/**
* @param \Generator<\Generator<T>> $generator
* Flatten a generator of page generators and yield each item in sequence.
*
* Increments the instance's fetched count for every yielded item.
*
* @param \Generator<\Generator<T>> $generator Generator that yields page generators; each page generator yields items of type T.
*
* @return \Generator<T>
* @return \Generator<T> Generator that yields items of type T from all pages in order.
*/
protected function execute(\Generator $generator): \Generator
{
Expand Down
12 changes: 9 additions & 3 deletions src/SourceCallbackAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@
class SourceCallbackAdapter implements SourceInterface
{
/**
* @param callable(int, int): \Generator<T> $callback
* Wraps a callable data source for use as a SourceInterface implementation.
*
* @param callable(int, int): \Generator<T> $callback A callable that accepts the 1-based page number and page size, and yields items of type `T`.
*/
public function __construct(private $callback)
{
}

/**
* @throws InvalidPaginationResultException
* Invoke the configured callback to produce a page of results as a Generator.
*
* Calls the adapter's callback with the provided page and page size and returns the resulting Generator.
*
* @throws InvalidPaginationResultException If the callback does not return a `\Generator`.
*
* @return \Generator<T>
* @return \Generator<T> A Generator that yields page results of type `T`.
*/
public function execute(int $page, int $pageSize): \Generator
{
Expand Down
11 changes: 9 additions & 2 deletions tests/ArraySource.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@
class ArraySource implements SourceInterface
{
/**
* @param array<T> $data
* Create a new ArraySource containing the provided items.
*
* @param array<T> $data The array of items to expose as the source.
*/
public function __construct(protected array $data)
{
}

/**
* @return \Generator<T>
* Provides the items for a specific page from the internal array.
*
* @param int $page Page number; values less than 1 are treated as 1.
* @param int $pageSize Number of items per page; if less than or equal to 0 no items are yielded.
*
* @return \Generator<T> A generator that yields the items for the requested page.
*/
public function execute(int $page, int $pageSize): \Generator
{
Expand Down