generated from Sammyjo20/package-template
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Paginator.php
414 lines (338 loc) · 11 KB
/
Paginator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
<?php
declare(strict_types=1);
namespace Saloon\PaginationPlugin;
use Iterator;
use Countable;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\Helpers\Helpers;
use InvalidArgumentException;
use Illuminate\Support\LazyCollection;
use GuzzleHttp\Promise\PromiseInterface;
use Saloon\PaginationPlugin\Contracts\Paginatable;
use Saloon\PaginationPlugin\Traits\HasAsyncPagination;
use Saloon\PaginationPlugin\Exceptions\PaginationException;
use Saloon\PaginationPlugin\Contracts\MapPaginatedResponseItems;
abstract class Paginator implements Iterator, Countable
{
/**
* The connector being paginated
*/
protected Connector $connector;
/**
* The request being paginated
*/
protected Request $request;
/**
* The start page when the paginator is rewound
*/
protected int $startPage = 1;
/**
* Internal Marker For The Current Page
*
* @deprecated Use $currentPage instead
*/
protected int $page = 1;
/**
* Denotes the current page
*/
protected int $currentPage = 0;
/**
* When using async this is the total number of pages
*/
protected ?int $totalPages = null;
/**
* Optional maximum number of pages the paginator is limited to
*/
protected ?int $maxPages = null;
/**
* The limit of results per-page
*/
protected ?int $perPageLimit = null;
/**
* The current response on the paginator
*/
protected ?Response $currentResponse = null;
/**
* Total results that have been paginated through
*/
protected int $totalResults = 0;
/**
* Should the pagination plugin check if there is an infinite loop
*/
protected bool $detectInfiniteLoop = true;
/**
* The last five response body checksums
*
* Used to determine if an infinite loop is happening
*
* @var array<int, string>
*/
protected array $lastFiveBodyChecksums = [];
/**
* Constructor
*/
public function __construct(Connector $connector, Request $request)
{
if (! $request instanceof Paginatable) {
throw new InvalidArgumentException(sprintf('The request must implement the `%s` interface to be used on paginators.', Paginatable::class));
}
$this->connector = $connector;
$this->request = clone $request;
// We'll register two middleware. One will force any requests to throw an exception
// if the request fails. This will prevent the rest of our paginator to keep
// on iterating if something goes wrong. The second middleware allows us
// to increment the total results which can be used to check if we
// are at the end of a page.
$this->request->middleware()
->onResponse(static fn (Response $response): Response => $response->throw())
->onResponse(function (Response $response): void {
$request = $response->getRequest();
$pageItems = $request instanceof MapPaginatedResponseItems
? $request->mapPaginatedResponseItems($response)
: $this->getPageItems($response, $request);
$this->totalResults += count($pageItems);
})
->onResponse(function (Response $response): void {
if ($this->detectInfiniteLoop === false || $this->isAsyncPaginationEnabled() === true) {
return;
}
// We'll start by creating a checksum of the body and appending
// it to the array of checksums. If the last five checksums
// are the same then we will throw an exception.
$this->lastFiveBodyChecksums[] = $this->getBodyChecksum($response);
if (count($this->lastFiveBodyChecksums) < 5) {
return;
}
$allValuesAreTheSame = count(array_unique($this->lastFiveBodyChecksums, SORT_REGULAR)) === 1;
// When there are five items in the array are the same, we'll throw an exception.
if ($allValuesAreTheSame === true) {
throw new PaginationException(
'Potential infinite loop detected! The last 5 requests have had exactly the same body. You can use the $detectInfiniteLoop property on your paginator to disable this check.'
);
}
// If all the values are not the same we will simply remove the
// oldest item from the array (the first)
array_shift($this->lastFiveBodyChecksums);
});
}
/**
* Get the current request
*/
public function current(): Response|PromiseInterface
{
$request = $this->applyPagination(clone $this->request);
if ($this->isAsyncPaginationEnabled() === false) {
return $this->currentResponse = $this->connector->send($request);
}
$promise = $this->connector->sendAsync($request);
// When the iterator is at the beginning, we need to force the first response to come
// back right away, so we can calculate the next pages we need to get.
if (is_null($this->currentResponse)) {
$this->currentResponse = $promise->wait();
}
return $promise;
}
/**
* Move to the next page
*/
public function next(): void
{
$this->page++;
$this->currentPage++;
}
/**
* Get the key of the paginator
*/
public function key(): int
{
return $this->currentPage;
}
/**
* Check if the paginator has another page to retrieve
*/
public function valid(): bool
{
if (isset($this->maxPages) && ($this->currentPage + 1) > $this->maxPages) {
return false;
}
if (is_null($this->currentResponse)) {
return true;
}
if ($this->isAsyncPaginationEnabled()) {
return ($this->currentPage + 1) <= $this->totalPages ??= $this->getTotalPages($this->currentResponse);
}
return $this->isLastPage($this->currentResponse) === false;
}
/**
* Rewind the iterator
*/
public function rewind(): void
{
$this->currentPage = max(0, $this->startPage - 1);
$this->page = $this->startPage;
$this->currentResponse = null;
$this->totalResults = 0;
$this->totalPages = null;
$this->onRewind();
}
/**
* Apply additional logic on rewind
*
* This may be resetting specific variables on the paginator classes
*/
protected function onRewind(): void
{
//
}
/**
* Iterate through the paginator items
*
* @return iterable<mixed, Response|PromiseInterface>
*/
public function items(): iterable
{
if ($this->isAsyncPaginationEnabled()) {
foreach ($this as $promise) {
yield $promise;
}
return;
}
/** @var Response $response */
foreach ($this as $response) {
$request = $response->getRequest();
$pageItems = $request instanceof MapPaginatedResponseItems
? $request->mapPaginatedResponseItems($response)
: $this->getPageItems($response, $request);
foreach ($pageItems as $item) {
yield $item;
}
}
}
/**
* Create a collection from the items
*/
public function collect(bool $throughItems = true): LazyCollection
{
return LazyCollection::make(function () use ($throughItems): iterable {
return $throughItems ? yield from $this->items() : yield from $this;
});
}
/**
* Get the total number of results
*/
public function getTotalResults(): int
{
return $this->totalResults;
}
/**
* Check if async pagination is enabled
*/
public function isAsyncPaginationEnabled(): bool
{
return in_array(HasAsyncPagination::class, Helpers::classUsesRecursive($this), true)
&& method_exists($this, 'getTotalPages')
&& property_exists($this, 'async')
&& $this->async === true;
}
/**
* Set the maximum number of pages the paginator will iterate over
*/
public function setMaxPages(?int $maxPages): Paginator
{
$this->maxPages = $maxPages;
return $this;
}
/**
* Set the per-page limit on the response
*/
public function setPerPageLimit(?int $perPageLimit): Paginator
{
$this->perPageLimit = $perPageLimit;
return $this;
}
/**
* Get the original request passed into the paginator
*/
public function getOriginalRequest(): Request
{
return $this->request;
}
/**
* Get page
*
* @deprecated Use currentPage() instead
*/
public function getPage(): int
{
return $this->page;
}
/**
* Get the current page
*/
public function getCurrentPage(): int
{
return $this->currentPage;
}
/**
* Set the start page of the paginator
*
* Used when the paginator is rewound
*/
public function setStartPage(int $startPage): static
{
$this->startPage = $startPage;
return $this;
}
/**
* Count the iterator
*/
public function count(): int
{
$this->rewind();
// When asynchronous pagination is enabled, we can call the `getTotalPages`
// method to count the number of pages. This reduces the number of API
// calls that we need to make.
if ($this->isAsyncPaginationEnabled() === true) {
/** @var PromiseInterface $promise */
$promise = $this->current();
return $this->getTotalPages($promise->wait());
}
// We are unable to use `iterator_count` because that method only calls
// the `next` and `valid` methods on the iterator, and it assumes the
// state of loaded items already exists - so in order to keep memory
// usage low, we should just iterate through each item and count.
$count = 0;
foreach ($this as $ignored) {
$count++;
}
return $count;
}
/**
* Get the checksum from the response body
*/
protected function getBodyChecksum(Response $response): string
{
$temporaryResource = $response->getRawStream();
$context = hash_init('md5');
hash_update_stream($context, $temporaryResource);
$checksum = hash_final($context);
fclose($temporaryResource);
return $checksum;
}
/**
* Apply the pagination to the request
*/
abstract protected function applyPagination(Request $request): Request;
/**
* Check if we are on the last page
*/
abstract protected function isLastPage(Response $response): bool;
/**
* Get the results from the page
*
* @return array<mixed, mixed>
*/
abstract protected function getPageItems(Response $response, Request $request): array;
}