Skip to content

Commit

Permalink
Support paginations with current page in URI path
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Jan 27, 2024
1 parent e762f75 commit 45fafb4
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 9 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,18 @@ If the API uses a query parameter different from `page` to specify the current p
LazyJsonPages::from($source)->pageName('current_page');
```

Otherwise, if the number of the current page is present in the URI path - for example `https://example.com/users/1` - we can chain the method `pageInPath()`:
Otherwise, if the number of the current page is present in the URI path - for example `https://example.com/users/page/1` - we can chain the method `pageInPath()`:

```php
LazyJsonPages::from($source)->pageInPath();
```

By default the last integer in the URI path is considered the page number. However we can customize the regular expression used to capture the page number, if need be:

```php
LazyJsonPages::from($source)->pageInPath('~/page/(\d+)$~');
```

Some API paginations may start with a page different from `1`. If that's the case, we can define the first page by chaining the method `firstPage()`:

```php
Expand Down
36 changes: 31 additions & 5 deletions src/Concerns/ResolvesPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace Cerbero\LazyJsonPages\Concerns;

use Cerbero\LazyJsonPages\Exceptions\InvalidPageInPathException;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;

/**
* The trait to resolve pages.
*/
Expand All @@ -17,20 +21,42 @@ protected function toPage(mixed $value, bool $onlyNumerics = true): string|int|n
return match (true) {
is_numeric($value) => (int) $value,
!is_string($value) || $value === '' => null,
!($query = parse_url($value, PHP_URL_QUERY)) => $onlyNumerics ? null : $value,
default => $this->pageFromQuery($query, $onlyNumerics),
!$parsedUri = parse_url($value) => $onlyNumerics ? null : $value,
default => $this->pageFromParsedUri($parsedUri, $onlyNumerics),
};
}

/**
* Retrieve the page from the given query.
* Retrieve the page from the given parsed URI.
*
* @return ($onlyNumerics is true ? int|null : string|int|null)
*/
protected function pageFromQuery(string $query, bool $onlyNumerics = true): string|int|null
protected function pageFromParsedUri(array $parsedUri, bool $onlyNumerics = true): string|int|null
{
parse_str($query, $parameters);
if ($pattern = $this->config->pageInPath) {
preg_match($pattern, $parsedUri['path'] ?? '', $matches);

return $this->toPage($matches[1] ?? null, $onlyNumerics);
}

parse_str($parsedUri['query'] ?? '', $parameters);

return $this->toPage($parameters[$this->config->pageName] ?? null, $onlyNumerics);
}

/**
* Retrieve the URI for the given page.
*/
protected function uriForPage(UriInterface $uri, string $page): UriInterface
{
if (!$pattern = $this->config->pageInPath) {
return Uri::withQueryValue($uri, $this->config->pageName, $page);
}

if (!preg_match($pattern, $path = $uri->getPath(), $matches, PREG_OFFSET_CAPTURE)) {
throw new InvalidPageInPathException($path, $pattern);
}

return $uri->withPath(substr_replace($path, $page, $matches[1][1], strlen($matches[1][0])));
}
}
4 changes: 1 addition & 3 deletions src/Concerns/SendsAsyncRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ protected function yieldRequests(UriInterface $uri, array $pages): Generator
$pages = $this->book->pullFailedPages() ?: $pages;

foreach ($pages as $page) {
$pageUri = Uri::withQueryValue($uri, $this->config->pageName, (string) $page);

yield $page => $request->withUri($pageUri);
yield $page => $request->withUri($this->uriForPage($uri, (string) $page));
}
}
}
1 change: 1 addition & 0 deletions src/Dtos/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class Config
public function __construct(
public readonly string $pointer,
public readonly string $pageName = 'page',
public readonly ?string $pageInPath = null,
public readonly int $firstPage = 1,
public readonly ?string $totalPagesKey = null,
public readonly ?string $totalItemsKey = null,
Expand Down
17 changes: 17 additions & 0 deletions src/Exceptions/InvalidPageInPathException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Cerbero\LazyJsonPages\Exceptions;

/**
* The exception thrown when a page cannot be found in the URI path.
*/
class InvalidPageInPathException extends LazyJsonPagesException
{
/**
* Instantiate the class.
*/
public function __construct(public readonly string $path, public readonly string $pattern)
{
parent::__construct("The pattern [{$pattern}] could not capture any page from the path [{$path}].");
}
}
10 changes: 10 additions & 0 deletions src/LazyJsonPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ public function pageName(string $name): self
return $this;
}

/**
* Set the pattern to capture the page in the URI path.
*/
public function pageInPath(string $pattern = '/(\d+)(?!.*\d)/'): self
{
$this->config['pageInPath'] = $pattern;

return $this;
}

/**
* Set the number of the first page.
*/
Expand Down
44 changes: 44 additions & 0 deletions tests/Feature/StructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

use Cerbero\LazyJsonPages\Exceptions\InvalidPageInPathException;
use Cerbero\LazyJsonPages\LazyJsonPages;

it('supports paginations with the current page in the URI path', function () {
$expectedItems = require fixture('items.php');
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users/page/1')
->pageInPath()
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests($expectedItems, [
'https://example.com/api/v1/users/page/1' => '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('supports a custom pattern for paginations with the current page in the URI path', function () {
$expectedItems = require fixture('items.php');
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users/page1')
->pageInPath('~/page(\d+)$~')
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests($expectedItems, [
'https://example.com/api/v1/users/page1' => 'lengthAware/page1.json',
'https://example.com/api/v1/users/page2' => 'lengthAware/page2.json',
'https://example.com/api/v1/users/page3' => 'lengthAware/page3.json',
]);
});

it('fails if it cannot capture the current page in the URI path', function () {
$expectedItems = require fixture('items.php');
$lazyCollection = LazyJsonPages::from('https://example.com/users')
->pageInPath()
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests($expectedItems, [
'https://example.com/users' => 'lengthAware/page1.json',
]);
})->throws(InvalidPageInPathException::class, 'The pattern [/(\d+)(?!.*\d)/] could not capture any page from the path [/users].');

0 comments on commit 45fafb4

Please sign in to comment.