Skip to content

Commit b0f87d6

Browse files
Apply retry logic to first-page bulk retrieval and add comprehensive pagination tests (#26)
* Fixing #23 for 429 on first page In the case you are running` $storybulkapi->all(),` and on the first page you have 429, the retrying mechanism is triggered now. It solves #23 * Update CHANGELOG.md
1 parent c50a46a commit b0f87d6

File tree

7 files changed

+826
-281
lines changed

7 files changed

+826
-281
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 1.0.6 - WIP
4+
- Bulk retrieval: Calling `all()` now applies the retry mechanism to the first page as well. Fixes #23.
5+
- Testing: Added extensive test cases covering bulk retrieval and pagination.
6+
37
## 1.0.5 - 2025-12-03
48
- Added `AssetField` class
59
- Fix `setAsset()`, accepting an `Asset` object for Asset Msanagement and converting into an `AssetField`

src/Endpoints/StoryBulkApi.php

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ public function __construct(
5555
* @return \Generator<Story>
5656
* @throws StoryblokApiException
5757
*/
58-
public function all(?StoriesParams $params = null, ?QueryFilters $filters = null, int $itemsPerPage = self::DEFAULT_ITEMS_PER_PAGE): \Generator
59-
{
60-
58+
public function all(
59+
?StoriesParams $params = null,
60+
?QueryFilters $filters = null,
61+
int $itemsPerPage = self::DEFAULT_ITEMS_PER_PAGE,
62+
): \Generator {
6163
$totalPages = null;
6264
$retryCount = 0;
6365
$page = new PaginationParams(self::DEFAULT_PAGE, $itemsPerPage);
@@ -71,7 +73,11 @@ public function all(?StoriesParams $params = null, ?QueryFilters $filters = null
7173
);
7274

7375
if ($response->isOk()) {
74-
$totalPages = $this->handleSuccessfulResponse($response, $totalPages, $itemsPerPage);
76+
$totalPages = $this->handleSuccessfulResponse(
77+
$response,
78+
$totalPages,
79+
$itemsPerPage,
80+
);
7581
yield from $this->getStoriesFromResponse($response);
7682
$page->incrementPage();
7783
$retryCount = 0;
@@ -80,13 +86,17 @@ public function all(?StoriesParams $params = null, ?QueryFilters $filters = null
8086
++$retryCount;
8187
}
8288
} catch (\Exception $e) {
83-
$this->logger->error('Error fetching stories', [
84-
'error' => $e->getMessage(),
85-
'page' => $page->page(),
89+
$this->logger->error("Error fetching stories", [
90+
"error" => $e->getMessage(),
91+
"page" => $page->page(),
8692
]);
87-
throw new StoryblokApiException('Failed to fetch stories: ' . $e->getMessage(), 0, $e);
93+
throw new StoryblokApiException(
94+
"Failed to fetch stories: " . $e->getMessage(),
95+
0,
96+
$e,
97+
);
8898
}
89-
} while ($page->page() <= $totalPages);
99+
} while ($totalPages === null || $page->page() <= $totalPages);
90100
}
91101

92102
/**
@@ -104,27 +114,35 @@ public function createStories(array $stories): \Generator
104114
while (true) {
105115
try {
106116
$response = $this->api->create($storyData);
107-
$this->logger->warning('Story created ' . $response->getResponseStatusCode());
117+
$this->logger->warning(
118+
"Story created " . $response->getResponseStatusCode(),
119+
);
108120
yield $response->data();
109121
$retryCount = 0;
110122
break;
111123
} catch (\Exception $e) {
112124
if ($e->getCode() === self::RATE_LIMIT_STATUS_CODE) {
113125
if ($retryCount >= self::MAX_RETRIES) {
114-
$this->logger->error('Max retries reached while creating story', [
115-
'story_name' => $storyData->name(),
116-
]);
126+
$this->logger->error(
127+
"Max retries reached while creating story",
128+
[
129+
"story_name" => $storyData->name(),
130+
],
131+
);
117132
throw new StoryblokApiException(
118-
'Rate limit exceeded maximum retries',
133+
"Rate limit exceeded maximum retries",
119134
self::RATE_LIMIT_STATUS_CODE,
120135
);
121136
}
122137

123-
$this->logger->warning('Rate limit reached while creating story, retrying...', [
124-
'retry_count' => $retryCount + 1,
125-
'max_retries' => self::MAX_RETRIES,
126-
'story_name' => $storyData->name(),
127-
]);
138+
$this->logger->warning(
139+
"Rate limit reached while creating story, retrying...",
140+
[
141+
"retry_count" => $retryCount + 1,
142+
"max_retries" => self::MAX_RETRIES,
143+
"story_name" => $storyData->name(),
144+
],
145+
);
128146

129147
$this->handleRateLimit();
130148
++$retryCount;
@@ -146,9 +164,8 @@ private function handleSuccessfulResponse(
146164
int $itemsPerPage,
147165
): int {
148166
if ($totalPages === null) {
149-
150167
$totalPages = (int) ceil($response->total() / $itemsPerPage);
151-
$this->logger->info('Total stories found: ' . $response->total());
168+
$this->logger->info("Total stories found: " . $response->total());
152169
}
153170

154171
return $totalPages;
@@ -159,18 +176,24 @@ private function handleSuccessfulResponse(
159176
*
160177
* @throws StoryblokApiException
161178
*/
162-
private function handleErrorResponse(StoryblokResponseInterface $response, int $retryCount): void
163-
{
164-
if ($response->getResponseStatusCode() === self::RATE_LIMIT_STATUS_CODE) {
179+
private function handleErrorResponse(
180+
StoryblokResponseInterface $response,
181+
int $retryCount,
182+
): void {
183+
if (
184+
$response->getResponseStatusCode() === self::RATE_LIMIT_STATUS_CODE
185+
) {
165186
if ($retryCount < self::MAX_RETRIES) {
166187
$this->handleRateLimit();
167188
} else {
168-
throw new StoryblokApiException('Rate limit exceeded maximum retries');
189+
throw new StoryblokApiException(
190+
"Rate limit exceeded maximum retries",
191+
);
169192
}
170193
} else {
171-
$this->logger->error('API error', [
172-
'status' => $response->getResponseStatusCode(),
173-
'message' => $response->getErrorMessage(),
194+
$this->logger->error("API error", [
195+
"status" => $response->getResponseStatusCode(),
196+
"message" => $response->getErrorMessage(),
174197
]);
175198
throw new StoryblokApiException($response->getErrorMessage());
176199
}
@@ -181,7 +204,7 @@ private function handleErrorResponse(StoryblokResponseInterface $response, int $
181204
*/
182205
protected function handleRateLimit(): void
183206
{
184-
$this->logger->warning('Rate limit reached, waiting before retry...');
207+
$this->logger->warning("Rate limit reached, waiting before retry...");
185208
sleep(self::RETRY_DELAY);
186209
}
187210

@@ -190,8 +213,9 @@ protected function handleRateLimit(): void
190213
*
191214
* @return \Generator<int, Story>
192215
*/
193-
private function getStoriesFromResponse(StoryblokResponseInterface $response): \Generator
194-
{
216+
private function getStoriesFromResponse(
217+
StoryblokResponseInterface $response,
218+
): \Generator {
195219
/** @var Stories $stories */
196220
$stories = $response->data();
197221
foreach ($stories as $story) {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"stories": [
3+
{
4+
"name": "My first post",
5+
"created_at": "2024-02-08T16:26:24.425Z",
6+
"published_at": "2024-02-08T16:27:05.705Z",
7+
"id": 440448565,
8+
"uuid": "e656e146-f4ed-44a2-8017-013e5a9d9395",
9+
"slug": "my-first-post",
10+
"full_slug": "posts/my-first-post",
11+
"sort_by_date": null,
12+
"position": 0,
13+
"tag_list": [],
14+
"is_startpage": false,
15+
"parent_id": 440448337,
16+
"meta_data": null,
17+
"group_id": "b913a671-f1e9-436a-bc5d-2795d2740198",
18+
"first_published_at": "2024-02-08T16:27:05.705Z",
19+
"release_id": null,
20+
"lang": "default",
21+
"path": null,
22+
"alternates": [
23+
{
24+
"id": 440452827,
25+
"name": "aaa",
26+
"slug": "bbb",
27+
"published": true,
28+
"full_slug": "bbb",
29+
"is_folder": false,
30+
"parent_id": 440452826
31+
}
32+
],
33+
"default_full_slug": "posts/my-first-post",
34+
"translated_slugs": [
35+
{
36+
"path": "posts/my-first-post-fr",
37+
"name": null,
38+
"lang": "fr",
39+
"published": null
40+
},
41+
{
42+
"path": "posts/my-first-post-de",
43+
"name": "My First post in DE",
44+
"lang": "de",
45+
"published": true
46+
}
47+
]
48+
},
49+
{
50+
"name": "My second post",
51+
"created_at": "2024-02-08T16:26:24.425Z",
52+
"published_at": "2024-02-08T16:27:05.705Z",
53+
"id": 440448565,
54+
"uuid": "e656e146-f4ed-44a2-8017-013e5a9d9395",
55+
"slug": "my-second-post",
56+
"full_slug": "posts/my-second-post",
57+
"sort_by_date": null,
58+
"position": 0,
59+
"tag_list": [],
60+
"is_startpage": false,
61+
"parent_id": 440448337,
62+
"meta_data": null,
63+
"group_id": "b913a671-f1e9-436a-bc5d-2795d2740198",
64+
"first_published_at": "2024-02-08T16:27:05.705Z",
65+
"release_id": null,
66+
"lang": "default",
67+
"path": null,
68+
"alternates": [
69+
{
70+
"id": 440452827,
71+
"name": "aaa",
72+
"slug": "bbb",
73+
"published": true,
74+
"full_slug": "aaa",
75+
"is_folder": false,
76+
"parent_id": 440452826
77+
}
78+
],
79+
"default_full_slug": "posts/my-second-post",
80+
"translated_slugs": [
81+
{
82+
"path": "posts/my-second-post-fr",
83+
"name": null,
84+
"lang": "fr",
85+
"published": null
86+
},
87+
{
88+
"path": "posts/my-second-post-de",
89+
"name": "Second post in DE",
90+
"lang": "de",
91+
"published": true
92+
}
93+
]
94+
}
95+
]
96+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"stories": [
3+
{
4+
"name": "My third post",
5+
"created_at": "2024-02-08T16:26:24.425Z",
6+
"published_at": "2024-02-08T16:27:05.705Z",
7+
"id": 440448565,
8+
"uuid": "e656e146-f4ed-44a2-8017-013e5a9d9395",
9+
"slug": "my-third-post",
10+
"full_slug": "posts/my-third-post",
11+
"sort_by_date": null,
12+
"position": 0,
13+
"tag_list": [],
14+
"is_startpage": false,
15+
"parent_id": 440448337,
16+
"meta_data": null,
17+
"group_id": "b913a671-f1e9-436a-bc5d-2795d2740198",
18+
"first_published_at": "2024-02-08T16:27:05.705Z",
19+
"release_id": null,
20+
"lang": "default",
21+
"path": null,
22+
"alternates": [
23+
{
24+
"id": 440452827,
25+
"name": "aaa",
26+
"slug": "bbb",
27+
"published": true,
28+
"full_slug": "bbb",
29+
"is_folder": false,
30+
"parent_id": 440452826
31+
}
32+
],
33+
"default_full_slug": "posts/my-third-post",
34+
"translated_slugs": [
35+
{
36+
"path": "posts/my-third-post-fr",
37+
"name": null,
38+
"lang": "fr",
39+
"published": null
40+
},
41+
{
42+
"path": "posts/my-third-post-de",
43+
"name": "My third post in DE",
44+
"lang": "de",
45+
"published": true
46+
}
47+
]
48+
},
49+
{
50+
"name": "My fourth post",
51+
"created_at": "2024-02-08T16:26:24.425Z",
52+
"published_at": "2024-02-08T16:27:05.705Z",
53+
"id": 440448565,
54+
"uuid": "e656e146-f4ed-44a2-8017-013e5a9d9395",
55+
"slug": "my-fourth-post",
56+
"full_slug": "posts/my-fourth-post",
57+
"sort_by_date": null,
58+
"position": 0,
59+
"tag_list": [],
60+
"is_startpage": false,
61+
"parent_id": 440448337,
62+
"meta_data": null,
63+
"group_id": "b913a671-f1e9-436a-bc5d-2795d2740198",
64+
"first_published_at": "2024-02-08T16:27:05.705Z",
65+
"release_id": null,
66+
"lang": "default",
67+
"path": null,
68+
"alternates": [
69+
{
70+
"id": 440452827,
71+
"name": "aaa",
72+
"slug": "bbb",
73+
"published": true,
74+
"full_slug": "aaa",
75+
"is_folder": false,
76+
"parent_id": 440452826
77+
}
78+
],
79+
"default_full_slug": "posts/my-fourth-post",
80+
"translated_slugs": [
81+
{
82+
"path": "posts/my-fourth-post-fr",
83+
"name": null,
84+
"lang": "fr",
85+
"published": null
86+
},
87+
{
88+
"path": "posts/my-fourth-post-de",
89+
"name": "fourth post in DE",
90+
"lang": "de",
91+
"published": true
92+
}
93+
]
94+
}
95+
]
96+
}

0 commit comments

Comments
 (0)