From 209c060e78b4862faa52e3dc165b7a17405e6434 Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori <andrea.marco.sartori@gmail.com> Date: Tue, 30 Jan 2024 19:49:44 +1000 Subject: [PATCH] Support custom pagination --- README.md | 35 ++++++++++++++++++ src/Dtos/Config.php | 10 ++++++ src/Exceptions/InvalidPaginationException.php | 23 ++++++++++++ src/LazyJsonPages.php | 10 ++++++ src/Paginations/AnyPagination.php | 12 +------ src/Paginations/CustomPagination.php | 36 +++++++++++++++++++ src/Paginations/Pagination.php | 16 ++++++--- tests/Feature/PaginationTest.php | 25 +++++++++++++ 8 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 src/Exceptions/InvalidPaginationException.php create mode 100644 src/Paginations/CustomPagination.php diff --git a/README.md b/README.md index ba83c47..56f63aa 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ composer require cerbero/lazy-json-pages * [🏛️ Pagination structure](#%EF%B8%8F-pagination-structure) * [📏 Length-aware paginations](#-length-aware-paginations) * [↪️ Cursor and next-page paginations](#%EF%B8%8F-cursor-and-next-page-paginations) +* [👽 Custom pagination](#-custom-pagination) * [🚀 Requests optimization](#-requests-optimization) * [💢 Errors handling](#-errors-handling) @@ -177,6 +178,40 @@ LazyJsonPages::from($source) > The documentation of this feature is a work in progress. +### 👽 Custom pagination + +Lazy JSON Pages provides several methods to extract items from the most popular pagination mechanisms. However if we need a custom solution, we can implement our own pagination. + +To implement a custom pagination, we need to extend `Pagination` and implement 1 method: + +```php +use Cerbero\LazyJsonPages\Paginations\Pagination; +use Traversable; + +class CustomPagination extends Pagination +{ + public function getIterator(): Traversable + { + // return a Traversable holding the paginated items + } +} +``` + +The parent class `Pagination` gives us access to 2 properties: +- `$source`: the mean pointing to the paginated JSON API (see [sources](#-sources)) +- `$config`: the configuration that we generated by chaining methods like `totalPages()` + +The method `getIterator()` defines the logic to extract paginated items in a memory-efficient way. Please refer to the [already existing paginations](https://github.com/cerbero90/json-parser/tree/master/src/Paginations) to see some implementations. + +Once the custom pagination is implemented, we can instruct Lazy JSON Pages to use it: + +```php +LazyJsonPages::from($source)->pagination(CustomPagination::class); +``` + +If you find yourself implementing the same custom pagination in different projects, feel free to send a PR and we will consider to support your custom pagination by default. Thank you in advance for any contribution! + + ### 🚀 Requests optimization > [!WARNING] diff --git a/src/Dtos/Config.php b/src/Dtos/Config.php index 9aab571..29244b8 100644 --- a/src/Dtos/Config.php +++ b/src/Dtos/Config.php @@ -4,10 +4,19 @@ namespace Cerbero\LazyJsonPages\Dtos; +use Cerbero\LazyJsonPages\Paginations\Pagination; use Closure; +/** + * The configuration + * + * @property-read class-string<Pagination> $pagination + */ final class Config { + /** + * Instantiate the class. + */ public function __construct( public readonly string $pointer, public readonly string $pageName = 'page', @@ -22,6 +31,7 @@ public function __construct( public readonly ?string $nextPageKey = null, public readonly ?int $lastPage = null, public readonly ?string $offsetKey = null, + public readonly ?string $pagination = null, public readonly int $async = 3, public readonly int $attempts = 3, public readonly ?Closure $backoff = null, diff --git a/src/Exceptions/InvalidPaginationException.php b/src/Exceptions/InvalidPaginationException.php new file mode 100644 index 0000000..97f4012 --- /dev/null +++ b/src/Exceptions/InvalidPaginationException.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace Cerbero\LazyJsonPages\Exceptions; + +use Cerbero\LazyJsonPages\Paginations\Pagination; + +/** + * The exception to throw when the given pagination is invalid. + */ +class InvalidPaginationException extends LazyJsonPagesException +{ + /** + * Instantiate the class. + */ + public function __construct(public readonly string $class) + { + $pagination = Pagination::class; + + parent::__construct("The class [{$class}] should extend [{$pagination}]."); + } +} diff --git a/src/LazyJsonPages.php b/src/LazyJsonPages.php index 0e543d7..c7307e7 100644 --- a/src/LazyJsonPages.php +++ b/src/LazyJsonPages.php @@ -163,6 +163,16 @@ public function offset(string $key = 'offset'): self return $this; } + /** + * Set the custom pagination. + */ + public function pagination(string $class): self + { + $this->config['pagination'] = $class; + + return $this; + } + /** * Fetch pages synchronously. */ diff --git a/src/Paginations/AnyPagination.php b/src/Paginations/AnyPagination.php index 2c01700..dd56b4e 100644 --- a/src/Paginations/AnyPagination.php +++ b/src/Paginations/AnyPagination.php @@ -19,23 +19,13 @@ class AnyPagination extends Pagination */ protected array $supportedPaginations = [ // CursorPagination::class, - // CustomPagination::class, + CustomPagination::class, // LastPageAwarePagination::class, - // LimitPagination::class, // LinkHeaderPagination::class, - // OffsetPagination::class, TotalItemsAwarePagination::class, TotalPagesAwarePagination::class, ]; - /** - * Determine whether this pagination matches the configuration. - */ - public function matches(): bool - { - return true; - } - /** * Yield the paginated items. * diff --git a/src/Paginations/CustomPagination.php b/src/Paginations/CustomPagination.php new file mode 100644 index 0000000..18339f2 --- /dev/null +++ b/src/Paginations/CustomPagination.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Cerbero\LazyJsonPages\Paginations; + +use Cerbero\LazyJsonPages\Exceptions\InvalidPaginationException; +use Traversable; + +/** + * The user-defined pagination. + */ +class CustomPagination extends LengthAwarePagination +{ + /** + * Determine whether the configuration matches this pagination. + */ + public function matches(): bool + { + return $this->config->pagination !== null; + } + + /** + * Yield the paginated items. + * + * @return Traversable<int, mixed> + */ + public function getIterator(): Traversable + { + if (!is_subclass_of($this->config->pagination, Pagination::class)) { + throw new InvalidPaginationException($this->config->pagination); + } + + yield from new $this->config->pagination($this->source, $this->config); + } +} diff --git a/src/Paginations/Pagination.php b/src/Paginations/Pagination.php index 9de6856..c9cfd80 100644 --- a/src/Paginations/Pagination.php +++ b/src/Paginations/Pagination.php @@ -32,11 +32,6 @@ abstract class Pagination implements IteratorAggregate */ protected readonly int $itemsPerPage; - /** - * Determine whether the configuration matches this pagination. - */ - abstract public function matches(): bool; - /** * Yield the paginated items. * @@ -44,10 +39,21 @@ abstract public function matches(): bool; */ abstract public function getIterator(): Traversable; + /** + * Instantiate the class. + */ final public function __construct( protected readonly Source $source, protected readonly Config $config, ) { $this->book = new Book(); } + + /** + * Determine whether the configuration matches this pagination. + */ + public function matches(): bool + { + return true; + } } diff --git a/tests/Feature/PaginationTest.php b/tests/Feature/PaginationTest.php index 7013a45..1ee9e58 100644 --- a/tests/Feature/PaginationTest.php +++ b/tests/Feature/PaginationTest.php @@ -1,6 +1,8 @@ <?php +use Cerbero\LazyJsonPages\Exceptions\InvalidPaginationException; use Cerbero\LazyJsonPages\LazyJsonPages; +use Cerbero\LazyJsonPages\Paginations\TotalPagesAwarePagination; it('supports paginations aware of their total pages', function () { $lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users') @@ -25,3 +27,26 @@ 'https://example.com/api/v1/users?page=3' => 'lengthAware/page3.json', ]); }); + +it('supports custom paginations', function () { + $lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users') + ->pagination(TotalPagesAwarePagination::class) + ->totalPages('meta.total_pages') + ->collect('data.*'); + + expect($lazyCollection)->toLoadItemsViaRequests([ + 'https://example.com/api/v1/users' => 'lengthAware/page1.json', + 'https://example.com/api/v1/users?page=2' => 'lengthAware/page2.json', + 'https://example.com/api/v1/users?page=3' => 'lengthAware/page3.json', + ]); +}); + +it('fails if an invalid custom pagination is provided', function () { + $lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users') + ->pagination('Invalid') + ->collect('data.*'); + + expect($lazyCollection)->toLoadItemsViaRequests([ + 'https://example.com/api/v1/users' => 'lengthAware/page1.json', + ]); +})->throws(InvalidPaginationException::class, 'The class [Invalid] should extend [Cerbero\LazyJsonPages\Paginations\Pagination].');