Skip to content

Commit 2f2836f

Browse files
feat: Add support for token based pagination strategy (#887)
1 parent aa4a2f1 commit 2f2836f

File tree

4 files changed

+512
-0
lines changed

4 files changed

+512
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
4+
namespace Twilio\Exceptions;
5+
6+
7+
/**
8+
* Exception thrown when a required key is missing from a response or metadata.
9+
*
10+
* This exception is typically thrown when the expected 'key' field is not present
11+
* in the response data or metadata, which may indicate a malformed or incomplete response.
12+
*
13+
* Example scenarios:
14+
* - When parsing API responses and the 'key' field is missing.
15+
* - When validating metadata and a required key is not found.
16+
*/
17+
class KeyErrorException extends TwilioException {
18+
19+
}

src/Twilio/TokenPaginationPage.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
4+
namespace Twilio;
5+
6+
7+
use Twilio\Exceptions\KeyErrorException;
8+
use Twilio\Http\Response;
9+
10+
/**
11+
* TokenPaginationPage is an abstract base class for handling paginated API responses
12+
* that use token-based pagination rather than traditional page numbers. These are part of
13+
* the new Twilio API Standards V1.
14+
*
15+
* Unlike the base {@see Page} class, which typically uses page numbers and URLs for navigation,
16+
* TokenPaginationPage manages pagination using tokens (e.g., nextToken, previousToken) provided
17+
* in the API response metadata. This allows for more flexible and scalable pagination, especially
18+
* for APIs that do not support offset-based pagination.
19+
*
20+
* Example expected response format with token metadata:
21+
* {
22+
* "meta": {
23+
* "key": "items",
24+
* "pageSize": 50,
25+
* "nextToken": "abc123",
26+
* "previousToken": "xyz789"
27+
* },
28+
* "items": [
29+
* { "id": 1, "name": "Item 1" },
30+
* { "id": 2, "name": "Item 2" }
31+
* // ...
32+
* ]
33+
* }
34+
*
35+
*/
36+
abstract class TokenPaginationPage extends Page {
37+
38+
protected $key;
39+
protected $pageSize;
40+
protected $nextToken;
41+
protected $previousToken;
42+
protected $url;
43+
protected $previousPageUrl;
44+
protected $nextPageUrl;
45+
46+
/**
47+
* TokenPaginationPage constructor.
48+
*
49+
* @param Version $version The API version object.
50+
* @param Response $response The HTTP response object.
51+
* @throws KeyErrorException If the 'key' metadata is missing.
52+
*/
53+
public function __construct(Version $version, Response $response) {
54+
parent::__construct($version, $response);
55+
56+
$httpClient = $version->getDomain()->getClient()->getHttpClient();
57+
58+
$this->url = '';
59+
if ($httpClient->lastRequest) {
60+
$fullUrl = $httpClient->lastRequest[CURLOPT_URL];
61+
// remove query parameters from url
62+
$parts = explode('?', $fullUrl);
63+
$this->url = $parts[0];
64+
}
65+
66+
$this->key = $this->getMeta('key');
67+
$this->pageSize = (int) $this->getMeta('pageSize');
68+
$this->nextToken = $this->getMeta('nextToken');
69+
$this->previousToken = $this->getMeta('previousToken');
70+
}
71+
72+
/**
73+
* Load the current page of records based on the 'key' metadata.
74+
*
75+
* @return array Array of records from the current page.
76+
* @throws KeyErrorException If the 'key' metadata is missing.
77+
*/
78+
protected function loadPage(): array {
79+
$this->key = $this->getMeta('key');
80+
if ($this->key) {
81+
return $this->payload[$this->key];
82+
}
83+
84+
throw new KeyErrorException('key not found in the response');
85+
}
86+
87+
/**
88+
* Construct the query string for pagination URLs.
89+
*
90+
* @param string|null $pageToken The token for the desired page.
91+
* @return string The constructed query string.
92+
*/
93+
protected function getQueryString(?string $pageToken): string {
94+
$params = [];
95+
if ($this->pageSize) {
96+
$params['pageSize'] = $this->pageSize;
97+
}
98+
if ($pageToken !== null && $pageToken !== '') {
99+
$params['pageToken'] = $pageToken;
100+
}
101+
$queryString = http_build_query($params);
102+
return $queryString ? '?' . $queryString : '';
103+
}
104+
105+
/**
106+
* Get the URL for the previous page of results.
107+
*
108+
* @return string|null The URL for the previous page, or null if there is no previous page.
109+
*/
110+
public function getPreviousPageUrl(): ?string {
111+
if (!$this->previousToken) {
112+
return null;
113+
}
114+
if (!$this->previousPageUrl) {
115+
$this->previousPageUrl = $this->url . $this->getQueryString($this->previousToken);
116+
}
117+
return $this->previousPageUrl;
118+
}
119+
120+
/**
121+
* Get the URL for the next page of results.
122+
*
123+
* @return string|null The URL for the next page, or null if there is no next page.
124+
*/
125+
public function getNextPageUrl(): ?string {
126+
if (!$this->nextToken) {
127+
return null;
128+
}
129+
if (!$this->nextPageUrl) {
130+
$this->nextPageUrl = $this->url . $this->getQueryString($this->nextToken);
131+
}
132+
return $this->nextPageUrl;
133+
}
134+
135+
public function __toString(): string {
136+
return '[TokenPaginationPage]';
137+
}
138+
139+
}

0 commit comments

Comments
 (0)