Getting started

Pagination

Every list surface has three methods: list for one page, listAll for everything, iterate for a stream.

Every paginated collection in the SDK is exposed three ways. They hit the same endpoint and differ only in how much they buffer.

MethodReturnsRequestsUse when
listEnvless\Result\PageExactly oneYou are rendering a page, or you only need a count
listAlllist of itemsAs many as it takesYou need the whole set in memory anyway
iterateGeneratorAs many as it takes, lazilyThe set is large, or you can stop early
php
use Envless\Envless;

$envless = new Envless();
$page = $envless->products->list(limit: 50);

echo count($page->items), PHP_EOL;
echo $page->pagination['limit'], ' ', $page->pagination['offset'], PHP_EOL;
echo $page->totalCount(), ' ', $page->hasMore() ? 'more' : 'last page', PHP_EOL;
php
use Envless\Envless;

$envless = new Envless();
$everything = $envless->products->listAll();

echo count($everything), PHP_EOL;
php
use Envless\Envless;

$envless = new Envless();

foreach ($envless->products->iterate() as $product) {
    if ($product['slug'] === 'billing') {
        break;
    }

    echo $product['name'], PHP_EOL;
}

iterate is the only one that can stop early. Breaking out of the foreach stops making requests, so scanning for one item in a large workspace costs one page, not all of them.

A Generator fetches the next page only when the loop gets there, and nothing at all is requested until the loop starts. That makes it the right shape for a long export: memory stays at one page however large the collection is.

php
use Envless\Envless;

$envless = new Envless();
$names = [];

foreach ($envless->products->iterate() as $product) {
    $names[] = $product['name'];

    if (count($names) === 10) {
        break;
    }
}

echo implode(', ', $names), PHP_EOL;

An Envless\Result\Page has items and pagination as readonly properties, plus hasMore() and totalCount(). It can be counted and looped over directly, and json_encode() turns it back into items and pagination. Its PHPStan type is generic over the item, so $page->items is a list<ProductResource> to PHPStan and Psalm.

Options

List methods take these as named arguments, after their positional slugs. Each list accepts only the options its route reads, so an option a route ignores is an Error for an unknown named parameter:

OptionTypeNotes
limit:int1 to 100. Defaults to 25 on the API. iterate and listAll default to 100 to minimise round trips, and clamp anything higher.
offset:intRows to skip. iterate and listAll use it as a starting point and then advance it themselves.
search:stringProducts, product projects, projects, environments, invites, members, roles, keys and webhooks. Each method page says what it matches.
order:stringname, asc or desc, on products, product projects, projects, environments, keys and webhooks.
sortBy:stringRoles only: name (the default), updated or created.
roleId:stringMembers only: keeps the members holding that role.
status:stringWebhook deliveries only: pending, processing, succeeded or failed.
product:stringProjects, environments, variables, variable history and versions: disambiguates a project slug that exists in more than one product.
timeout:int or floatBounds each request of the walk, not the whole walk.

A limit: outside 1 to 100 is refused by the API with 400 request.invalid_pagination rather than being clamped server-side, and field on the exception names the offending parameter.

How the walk terminates

listAll and iterate stop on either of two conditions, which matters because it makes them safe against a server that keeps saying hasMore:

hasMore is false

The normal case.

A page comes back empty

Even if hasMore is still true. Without this a bad cursor would loop forever.

The offset advances by the number of items actually received, not by limit, so a short page does not skip rows. A failure on any page fails the whole listAll call, and the rows it had already collected are lost; iterate has already handed over the earlier items, so it throws at the page that failed.

Secondary collections

A namespace can paginate more than one collection. Those get their own trio, named after what they read.

NamespacePrimarySecondary
productslist, listAll, iteratelistProjects, listAllProjects, iterateProjects and access->list, access->listAll, access->iterate
variableslist, listAll, iteratelistHistory, listAllHistory, iterateHistory
versionslist, listAll, iteratelistEntries, listAllEntries, iterateEntries
webhookslist, listAll, iteratelistDeliveries, listAllDeliveries, iterateDeliveries

Two endpoints look like lists but are not paginated, because the whole set is small and fixed. Both return a plain array rather than a page:

MethodReturns
roles->listPermissions()PermissionsResource with the full catalogue, a byResource index, and the system role sets
webhooks->listEventTypes()EventTypesResource with eventTypes and totalCount

One collection carries an extra field

products->access->list() returns an Envless\Result\ProductAccessPage, which is a normal page plus the product's privacy flag, read as isPrivate. That flag tells you whether the grants matter at all: on a public product every member has access and the list is informational.

php
use Envless\Envless;

$envless = new Envless();
$access = $envless->products->access->list('internal-tools');

if (!$access->isPrivate) {
    echo 'Public product, every member already has access', PHP_EOL;
}

foreach ($access->items as $entry) {
    echo $entry['id'], PHP_EOL;
}

access->listAll() and access->iterate() only stream items, so they drop the flag. If you need it, use access->list().

Counting without fetching

totalCount() is the size of the full filtered set, not the page, so a single one-row request is enough to count.

php
use Envless\Envless;

$envless = new Envless();

echo $envless->members->list(limit: 1)->totalCount(), ' members', PHP_EOL;