Skip to content

Commit 828ccbf

Browse files
authored
Merge pull request #89 from DirectoryTree/cancel-chunk-and-each
Add support for breaking out of `chunk()` or `each()` but returning `false` in callback
2 parents b144729 + cb12556 commit 828ccbf

File tree

4 files changed

+215
-6
lines changed

4 files changed

+215
-6
lines changed

src/MessageQuery.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,13 @@ public function append(string $message, mixed $flags = null): int
9292
*/
9393
public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void
9494
{
95-
$this->chunk(fn (MessageCollection $messages) => (
96-
$messages->each($callback)
97-
), $chunkSize, $startChunk);
95+
$this->chunk(function (MessageCollection $messages) use ($callback) {
96+
foreach ($messages as $key => $message) {
97+
if ($callback($message, $key) === false) {
98+
return false;
99+
}
100+
}
101+
}, $chunkSize, $startChunk);
98102
}
99103

100104
/**
@@ -134,7 +138,10 @@ public function chunk(callable $callback, int $chunkSize = 10, int $startChunk =
134138
break;
135139
}
136140

137-
$callback($hydrated, $page);
141+
// If the callback returns false, break out.
142+
if ($callback($hydrated, $page) === false) {
143+
break;
144+
}
138145
}
139146

140147
// Restore the original state.

src/Testing/FakeMessageQuery.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,36 @@ public function append(string $message, mixed $flags = null): int
8181
*/
8282
public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void
8383
{
84-
$this->get()->each($callback);
84+
$this->chunk(function (MessageCollection $messages) use ($callback) {
85+
foreach ($messages as $key => $message) {
86+
if ($callback($message, $key) === false) {
87+
return false;
88+
}
89+
}
90+
}, $chunkSize, $startChunk);
8591
}
8692

8793
/**
8894
* {@inheritDoc}
8995
*/
9096
public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = 1): void
9197
{
92-
$this->get()->chunk($chunkSize)->each($callback);
98+
$page = $startChunk;
99+
100+
foreach ($this->get()->chunk($chunkSize) as $chunk) {
101+
if ($page < $startChunk) {
102+
$page++;
103+
104+
continue;
105+
}
106+
107+
// If the callback returns false, break out.
108+
if ($callback($chunk, $page) === false) {
109+
break;
110+
}
111+
112+
$page++;
113+
}
93114
}
94115

95116
/**

tests/Unit/MessageQueryTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,94 @@ function query(?Mailbox $mailbox = null): MessageQuery
9090
expect($query->oldest())->toBe($query);
9191
expect($query->newest())->toBe($query);
9292
});
93+
94+
test('each breaks when callback returns false', function () {
95+
$stream = new FakeStream;
96+
$stream->open();
97+
98+
$stream->feed([
99+
'* OK Welcome to IMAP',
100+
'TAG1 OK Logged in',
101+
'* SEARCH 1 2 3 4 5',
102+
'TAG2 OK SEARCH completed',
103+
'* 5 FETCH (UID 5 FLAGS () BODY[HEADER] {0}',
104+
'',
105+
' BODY[TEXT] {0}',
106+
'',
107+
')',
108+
'* 4 FETCH (UID 4 FLAGS () BODY[HEADER] {0}',
109+
'',
110+
' BODY[TEXT] {0}',
111+
'',
112+
')',
113+
'TAG3 OK FETCH completed',
114+
]);
115+
116+
$mailbox = Mailbox::make();
117+
$mailbox->connect(new ImapConnection($stream));
118+
119+
$processedUids = [];
120+
121+
query($mailbox)->each(function ($message) use (&$processedUids) {
122+
$processedUids[] = $message->uid();
123+
124+
// Break after processing the first message (which will be UID 5 due to desc order)
125+
if ($message->uid() === 5) {
126+
return false;
127+
}
128+
}, 2); // Use chunk size of 2
129+
130+
// Should only process the first message (UID 5 due to desc order)
131+
expect($processedUids)->toBe([5]);
132+
});
133+
134+
test('chunk breaks when callback returns false', function () {
135+
$stream = new FakeStream;
136+
$stream->open();
137+
138+
$stream->feed([
139+
'* OK Welcome to IMAP',
140+
'TAG1 OK Logged in',
141+
'* SEARCH 1 2 3 4 5',
142+
'TAG2 OK SEARCH completed',
143+
'* 5 FETCH (UID 5 FLAGS () BODY[HEADER] {0}',
144+
'',
145+
' BODY[TEXT] {0}',
146+
'',
147+
')',
148+
'* 4 FETCH (UID 4 FLAGS () BODY[HEADER] {0}',
149+
'',
150+
' BODY[TEXT] {0}',
151+
'',
152+
')',
153+
'TAG3 OK FETCH completed',
154+
'* 3 FETCH (UID 3 FLAGS () BODY[HEADER] {0}',
155+
'',
156+
' BODY[TEXT] {0}',
157+
'',
158+
')',
159+
'* 2 FETCH (UID 2 FLAGS () BODY[HEADER] {0}',
160+
'',
161+
' BODY[TEXT] {0}',
162+
'',
163+
')',
164+
'TAG4 OK FETCH completed',
165+
]);
166+
167+
$mailbox = Mailbox::make();
168+
$mailbox->connect(new ImapConnection($stream));
169+
170+
$processedChunks = [];
171+
172+
query($mailbox)->chunk(function ($messages, $page) use (&$processedChunks) {
173+
$processedChunks[] = $page;
174+
175+
// Break after processing the first chunk
176+
if ($page === 1) {
177+
return false;
178+
}
179+
}, 2); // Use chunk size of 2
180+
181+
// Should only process the first chunk (page 1)
182+
expect($processedChunks)->toBe([1]);
183+
});

tests/Unit/Testing/FakeMessageQueryTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,93 @@
173173
expect($query->find(2))->not->toBeNull();
174174
expect($query->find(4))->not->toBeNull();
175175
});
176+
177+
test('each breaks when callback returns false', function () {
178+
$folder = new FakeFolder('INBOX', messages: [
179+
new FakeMessage(1, [], 'Message 1'),
180+
new FakeMessage(2, [], 'Message 2'),
181+
new FakeMessage(3, [], 'Message 3'),
182+
new FakeMessage(4, [], 'Message 4'),
183+
new FakeMessage(5, [], 'Message 5'),
184+
]);
185+
186+
$query = new FakeMessageQuery($folder);
187+
$processedUids = [];
188+
189+
$query->each(function ($message) use (&$processedUids) {
190+
$processedUids[] = $message->uid();
191+
192+
// Break after processing the third message
193+
if ($message->uid() === 3) {
194+
return false;
195+
}
196+
}, 2); // Use chunk size of 2
197+
198+
// Should process messages 1, 2, and 3, then break
199+
expect($processedUids)->toBe([1, 2, 3]);
200+
});
201+
202+
test('chunk breaks when callback returns false', function () {
203+
$folder = new FakeFolder('INBOX', messages: [
204+
new FakeMessage(1, [], 'Message 1'),
205+
new FakeMessage(2, [], 'Message 2'),
206+
new FakeMessage(3, [], 'Message 3'),
207+
new FakeMessage(4, [], 'Message 4'),
208+
new FakeMessage(5, [], 'Message 5'),
209+
]);
210+
211+
$query = new FakeMessageQuery($folder);
212+
$processedChunks = [];
213+
214+
$query->chunk(function ($messages, $page) use (&$processedChunks) {
215+
$processedChunks[] = $page;
216+
217+
// Break after processing the second chunk
218+
if ($page === 2) {
219+
return false;
220+
}
221+
}, 2); // Use chunk size of 2
222+
223+
// Should process chunks 1 and 2, then break (chunk 3 should not be processed)
224+
expect($processedChunks)->toBe([1, 2]);
225+
});
226+
227+
test('each processes all messages when callback never returns false', function () {
228+
$folder = new FakeFolder('INBOX', messages: [
229+
new FakeMessage(1, [], 'Message 1'),
230+
new FakeMessage(2, [], 'Message 2'),
231+
new FakeMessage(3, [], 'Message 3'),
232+
]);
233+
234+
$query = new FakeMessageQuery($folder);
235+
$processedUids = [];
236+
237+
$query->each(function ($message) use (&$processedUids) {
238+
$processedUids[] = $message->uid();
239+
// Never return false
240+
});
241+
242+
// Should process all messages
243+
expect($processedUids)->toBe([1, 2, 3]);
244+
});
245+
246+
test('chunk processes all chunks when callback never returns false', function () {
247+
$folder = new FakeFolder('INBOX', messages: [
248+
new FakeMessage(1, [], 'Message 1'),
249+
new FakeMessage(2, [], 'Message 2'),
250+
new FakeMessage(3, [], 'Message 3'),
251+
new FakeMessage(4, [], 'Message 4'),
252+
new FakeMessage(5, [], 'Message 5'),
253+
]);
254+
255+
$query = new FakeMessageQuery($folder);
256+
$processedChunks = [];
257+
258+
$query->chunk(function ($messages, $page) use (&$processedChunks) {
259+
$processedChunks[] = $page;
260+
// Never return false
261+
}, 2); // Use chunk size of 2
262+
263+
// Should process all chunks (1, 2, 3)
264+
expect($processedChunks)->toBe([1, 2, 3]);
265+
});

0 commit comments

Comments
 (0)