Пагинация
Одна страница, все страницы или по одному элементу за раз в каждом списке с постраничной выдачей.
list, listAll и iterate
У каждого списка с постраничной выдачей есть три метода. list получает одну страницу и возвращает OpenEmail\Result\Page. listAll следует за курсором через все страницы и возвращает один массив. iterate обходит те же страницы по одному элементу и возвращает Generator, который запрашивает следующую страницу, только когда вы до неё дойдёте. Все три принимают фильтры списка, limit:, cursor: и apiKey:.
$page = $client->emails->list(status: 'failed', limit: 50); foreach ($page as $email) { echo $email['id'], ' ', $email['lastError'] ?? '', PHP_EOL;} $failures = $client->emails->listAll(status: 'failed'); foreach ($client->emails->iterate(status: 'failed') as $email) { echo $email['id'], PHP_EOL;} echo count($failures), ' ', $page->hasMore ? 'more to come' : 'that is all', PHP_EOL;Те же три имени повторяются везде, где у пространства имён больше одного списка, и называются по списку, который обходят: listEvents, listAllEvents и iterateEvents в emails, listDeliveries, listAllDeliveries и iterateDeliveries в webhooks и так далее.
OpenEmail\Result\Page
itemsarray- Строки этой страницы, извлечённые из конверта `data` API, каждая в виде ассоциативного массива. Пусто, если на странице ничего нет.
hasMorebool- Есть ли следующая страница. Если API не присылает `hasMore`, значение истинно ровно тогда, когда есть `nextCursor`.
nextCursorstring or null- Что передать обратно как `cursor:` для следующей страницы. На последней странице null.
Страница неизменяема: её свойства объявлены как readonly. Она также реализует IteratorAggregate и Countable, поэтому foreach ($page as $item) обходит её строки, а count($page) их считает.
Generator
iterate ничего не запрашивает, пока вы не начнёте его обходить, и запрашивает следующую страницу, только когда все элементы текущей уже выданы, поэтому всё, что останавливается раньше, останавливает и запросы: break завершает обход, как и возврат из функции, в которой находится цикл.
$latest = []; foreach ($client->emails->iterate(status: 'failed', limit: 100) as $email) { $latest[] = $email; if (count($latest) === 10) { break; }} $invoice = null; foreach ($client->emails->iterate(status: 'failed') as $email) { if (($email['tags']['invoice'] ?? null) === 'inv_2026_09_4192') { $invoice = $email; break; }} echo count($latest), ' ', $invoice['id'] ?? 'not found', PHP_EOL;Всё, чему нужны все элементы, например iterator_to_array(), вызванный на Generator, читает все страницы, прежде чем вернуть результат, как это делает listAll.
Generator можно обойти только один раз. Повторный обход выбрасывает исключение, поэтому, если элементы снова нужны, вызовите iterate ещё раз или сохраните сами элементы, а не Generator.
Продолжение с курсора
Курсор непрозрачен. Сохраните nextCursor последней прочитанной страницы и передайте его обратно как cursor:, чтобы продолжить с этого места в более позднем запросе или в другом процессе. listAll и iterate тоже принимают cursor: и начинают обход после него.
$firstPage = $client->emails->list(status: 'failed', limit: 25);$saved = $firstPage->nextCursor; if ($saved !== null) { $rest = $client->emails->listAll(status: 'failed', cursor: $saved); echo count($rest), PHP_EOL;}Курсор принадлежит списку и фильтрам, из которых он получен, поэтому отправляйте вместе с ним те же фильтры. Курсор, который список не может распознать, отклоняется с invalid_cursor, и тогда остаётся начать заново без курсора.
limit:
limit: является размером каждой страницы, а не общим количеством. В list это число возвращаемых строк. В listAll и iterate это число строк, которое запрашивает каждый запрос, поэтому большее значение означает меньше обращений к серверу за те же строки. У каждого списка свой диапазон и значение по умолчанию, чаще всего от 1 до 100 и 25, если ничего не передано, а значение вне диапазона отклоняется, а не подрезается. Диапазон указан в справочнике по каждому списку.
Когда обход останавливается
- Когда страница сообщает, что
hasMoreравно false. - Когда страница не несёт
nextCursor, поскольку страница, которая заявляет о продолжении, но не называет курсора, зациклилась бы навсегда. - Когда API возвращает курсор, по которому обход уже прошёл, по той же причине.
Каждая страница является GET, поэтому, как любое чтение, она сама повторяется, прежде чем что-либо выбросит исключение. Сбой, переживший повторы, выбрасывается из listAll, а уже полученные элементы отбрасываются. В iterate элементы предыдущих страниц к этому моменту уже выданы, поэтому сделайте действия цикла безопасными для повторного выполнения или листайте через list и сохраняйте каждый nextCursor, чтобы вторая попытка могла начать с того места, где остановилась первая.
Цепочки и черновики
threads->list и drafts->list вместе со своими listAll и iterate листают с помощью pageToken и nextPageToken из API, а не курсора. Клиент скрывает эту разницу: передавайте токен как cursor: и читайте его из nextCursor.
$page = $client->threads->list(folder: 'inbox', limit: 50);$later = $page->hasMore ? $client->threads->list(folder: 'inbox', limit: 50, cursor: $page->nextCursor) : null; echo count($page), ' ', $later === null ? 0 : count($later), PHP_EOL;Сервер предлагает токен всякий раз, когда страница возвращается заполненной, поэтому hasMore может быть true на странице, которая окажется последней, и тогда следующий вызов не вернёт ни одного элемента.
Страницы с дополнительными полями
Несколько списков отвечают не только строками и возвращают собственный объект из OpenEmail\Result вместо Page. Каждый из них неизменяем, реализует IteratorAggregate по своим строкам и Countable.
| Метод | Возвращает | Что добавляет |
|---|---|---|
| addresses->list | AddressBookPage | addresses вместо items, а также unrestricted и domains, с hasMore и nextCursor. |
| addresses->listAll | AddressBook | Все адреса в addresses, с unrestricted и domains в том виде, в каком их сообщила последняя страница. Это единственный listAll, который возвращает всю адресную книгу, а не массив. addresses->iterate передаёт только адреса. |
| contacts->listPeople | PeoplePage | seen, равное false, когда ключ не может читать адреса, встреченные в почте. listAllPeople и iteratePeople возвращают только людей. |
| tempMail->listMessages | TempMessagesPage | expiresAt, момент, когда срок ящика истекает. listAllMessages и iterateMessages возвращают только сообщения. |
| templates->listSends | TemplateSends | Постраничная выдача по номеру, а не по курсору: items, total, page и pageSize. Следующую страницу запрашивайте через page:. |
| emails->sendBatch | BatchResult | Не страница: items, по одному на каждое отправленное вами сообщение, со счётчиками sent и failed. |
Списки, которые возвращают массив из API
Некоторые списки листают по смещению, по номеру страницы или по собственному числовому курсору и возвращают декодированное тело как есть, массив с data, а не Page. У них нет listAll или iterate, поэтому листать их приходится самостоятельно.
| Метод | Листается через | Что возвращается |
|---|---|---|
| exports->list | limit: и offset: | data, total и hasMore. |
| imports->listFailures | after: и limit: | data и nextCursor, целое число для передачи обратно как after:, равное null на последней странице. |
| subscriptions->list и subscriptions->listDomains | limit: и offset: | data, total, counts и hasMore. |
| billing->listInvoices | page: и limit: | data, total, page, limit, hasMore и metered. |
$offset = 0; do { $batch = $client->subscriptions->list(status: 'active', limit: 50, offset: $offset); foreach ($batch['data'] as $row) { echo $row['senderEmail'], ' ', $row['total'], PHP_EOL; } $offset += count($batch['data']);} while ($batch['hasMore'] && $batch['data'] !== []);Список без постраничной выдачи, например languages->list, labels->listColors или roles->listPermissions, сразу возвращает свои строки обычным списком.