saloonphp/pagination-plugin
Adds paginated response support to SaloonPHP. Provides a PaginationPlugin with helpful abstractions to iterate through pages and results when working with APIs that return paginated data, keeping pagination logic out of your connectors and requests.
Install the Package
composer require saloonphp/pagination-plugin
Ensure your saloon/saloon version is v3+ (check composer.json).
Register the Plugin
In your connector’s extend() method:
use Saloon\Contracts\Connector;
use Saloon\Pagination\PaginationPlugin;
public function extend(): void
{
$this->withPlugin(new PaginationPlugin());
}
Define a Pagination Strategy
For a standard page/limit API (e.g., GitHub):
use Saloon\Pagination\Strategies\PageLimitStrategy;
$this->withPaginationStrategy(new PageLimitStrategy(
pageParam: 'page',
limitParam: 'per_page',
responseKey: 'data'
));
First Use Case: Fetch All Paginated Data
$response = $this->call(new GetUsersRequest());
$users = $response->paginate()->all(); // Returns a flattened array
src folder for PaginationPlugin and Strategies).saloonphp/github-connector).tests/ folder) demonstrates edge cases like cursor-based pagination or async chunking.// Define in connector
$this->withPaginationStrategy(new PageLimitStrategy(
pageParam: 'page',
limitParam: 'per_page',
responseKey: 'items'
));
// Fetch paginated data
$paginator = $this->call(new GetPostsRequest())->paginate();
$posts = $paginator->all(); // Flattens all pages
$posts = $paginator->take(50); // Limits to 50 items
$this->withPaginationStrategy(new CursorStrategy(
cursorParam: 'cursor',
responseKey: 'edges',
cursorKey: 'cursor',
dataKey: 'node'
));
$paginator = $this->call(new GetCommentsRequest())->paginate();
$comments = $paginator->each(fn ($comment) => $this->process($comment));
$this->withPaginationStrategy(new LinkHeaderStrategy(
linkHeaderKey: 'Link',
nextLinkPattern: '/rel="next"'
));
$paginator = $this->call(new GetLargeDatasetRequest())->paginate();
$paginator->chunk(100, function (array $chunk) {
ProcessLargeChunkJob::dispatch($chunk);
});
Cache Paginated Results:
$users = Cache::remember('api_users_all', now()->addHours(1), function () {
return $this->call(new GetUsersRequest())->paginate()->all();
});
Queue Async Processing:
$paginator->each(function ($item) {
ProcessItemJob::dispatch($item);
});
Laravel Collections Integration:
$collection = collect($paginator->all())->map(...);
Extend PaginationStrategy for niche APIs:
class CustomStrategy extends PaginationStrategy
{
public function getNextPageData(array $response): ?array
{
return $response['meta']['next_page'] ?? null;
}
public function getPageParam(): string
{
return 'offset';
}
}
Mock paginated responses in Pest:
$mock = $this->mockConnector()
->shouldReceive('call')
->once()
->andReturn(new Response(
body: ['data' => [['id' => 1]], 'links' => ['next' => '/page/2']]
));
$paginator = $mock->paginate();
$paginator->assertHasNextPage();
Infinite Loop Detection
$strategy = new PageLimitStrategy(/* ... */);
$strategy->disableLoopDetection();
rewind() manually if needed (reset checksums):
$paginator->rewind();
Async Memory Leaks
each(), chunk()) may hold large datasets in memory if not processed immediately.$paginator->chunk(1000, fn ($chunk) => $this->processChunk($chunk));
Deprecated Properties
page property (now deprecated). Update to startPage:
// Old (deprecated)
$paginator->setPage(2);
// New
$paginator->setStartPage(2);
Cursor Strategy Quirks
data.cursor). Ensure cursorKey is correct.dd($paginator->getLastResponse()) to inspect the response structure.PHP 8.5+ Type Safety
$paginator->take(10); // Now explicitly returns `Paginator`
Inspect Responses
$response = $this->call(new GetDataRequest());
dd($response->paginate()->getLastResponse()); // Debug raw response
Enable Verbose Logging
$this->withPlugin(new PaginationPlugin([
'debug' => true,
]));
Checksum Mismatches
$paginator->resetChecksums();
Performance Profiling
$paginator->getCallCount()).Custom Pagination Strategies
Extend Saloon\Pagination\PaginationStrategy to support:
after, before).offset=0&limit=100).Event Hooks
Listen to pagination events (e.g., PaginatorBeforeFetch):
$this->withPlugin(new PaginationPlugin([
'events' => [
PaginatorBeforeFetch::class => fn ($paginator) => $this->log($paginator),
],
]));
Middleware Integration Add request/response middleware to modify pagination behavior:
$this->withPlugin(new PaginationPlugin([
'middleware' => [
new AddCustomHeaderMiddleware(),
],
]));
Laravel Service Provider Register the plugin globally for all connectors:
// app/Providers/SaloonServiceProvider.php
public function register(): void
{
Saloon::extend(function ($connector) {
$connector->withPlugin(new PaginationPlugin());
});
}
config/pagination.php) and inject them dynamically.PageLimitStrategy + LinkHeaderStrategy) for APIs with fallback mechanisms.RateLimiter with pagination to avoid hitting API limits:
$this->withRateLimiter(new SaloonRateLimiter(1000)); // 1000 requests/min
How can I help you explore Laravel packages today?