Skip to content

Conversation

@birdcar
Copy link
Contributor

@birdcar birdcar commented Jan 6, 2026

Description

Previously, when returning data from a resource that was paginated, we would parse the pagination args off the response, loop over the "data" value in the response, and map each item in the JSON array to a specific resource. An example of this would be in the listUsers() method of the UserManagement class (truncated example below):

$users = [];

list($before, $after) = Util\Request::parsePaginationArgs($response);
foreach ($response["data"] as $responseData) {
    \array_push($users, Resource\User::constructFromResponse($responseData));
}

return [$before, $after, $users];

Performing this pattern over and over again resulted in a lot of duplicate code that was doing basically nothing more than an array_map.

Additionally, this return is extremely limited and forces the user into a limited and specific pattern of either bare array destructuring:

[$before, $after, $users] = $userManagement->listUsers();

Or dealing with 0-indexed array values:

$result = $userManagement->listUsers();
$before = $result[0];
$after = $result[1];
$users = $result[2];

If for example they just want the first 5 users and don't care about paginating, this means they'd need to either write a destructuring expression that has empty values:

// Huh?
[,,$users] = $userManagement->listUsers(limit: 5);

Or they'd have to drop down to:

$results = $userManagement->listUsers(limit: 5);
// How do I discover or know what this index is?
$users = $results[2];

To fix both of these issues, without affecting current library consumers, I'm proposing that we create a Resource\PaginatedResource class that:

  1. DRYs and standardizes the creation of a paginated resource (e.g. now you can just return Resource\PaginatedResource::constructFromResponse($response, Resource\User::class, 'users');)
  2. Handles the resource mapping from the data array
  3. Continues to allow for bare destructuring (backwards compatible)
  4. Introduces named destructuring (e.g $result["after"] or ["users" => $fiveUsers] = $userManagement->listUsers(limit:5))
  5. Introduces fluent property access (e.g. $result->after or $result->users)

The change is fully backwards compatible, cleans up existing resource code and allows for developers to use the library in whichever code style is consistent with their project.

For example, it lets you turn this code:

[$before, $after, $users] = $userManagement->listUsers();

while ($after) {
    [$before, $after, $currentPage] = $sso->listConnections(
        limit: 100,
        after: $after,
        order: "desc"
    );

    $users = array_merge($users, $currentPage);
}

Into this code:

$users = [];
$after = null;

do {
    $result = $userManagement->listUsers(after: $after, limit: 100, order: "desc");
    $users = array_merge($users, $result->users);
    $after = $result->after;
} while ($after !== null);

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.

@birdcar birdcar self-assigned this Jan 6, 2026
@birdcar birdcar requested a review from a team as a code owner January 6, 2026 23:50
@birdcar birdcar requested a review from mattgd January 6, 2026 23:50
greptile-apps[bot]

This comment was marked as outdated.

@workos workos deleted a comment from greptile-apps bot Jan 6, 2026
@birdcar birdcar force-pushed the birdcar/better-paginated-resources branch from 30b94ee to d35cb34 Compare January 7, 2026 00:12
@birdcar
Copy link
Contributor Author

birdcar commented Jan 7, 2026

@greptileai

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 7, 2026

Greptile Overview

Greptile Summary

Introduced PaginatedResource class to eliminate code duplication across paginated API methods and provide multiple ergonomic access patterns for developers.

Key Changes:

  • Created new Resource\PaginatedResource class implementing ArrayAccess and IteratorAggregate interfaces
  • Refactored all paginated list methods across 5 service classes (DirectorySync, Organizations, SSO, UserManagement, Vault) to use the new class
  • Replaced ~70 lines of duplicate pagination code with single-line PaginatedResource::constructFromResponse() calls

Benefits:

  • Backwards compatible: Existing code using bare destructuring [$before, $after, $data] = $result continues to work via ArrayAccess implementation
  • Named destructuring: New pattern ["users" => $users, "after" => $after] = $result improves readability
  • Fluent access: Object property access $result->users, $result->after provides most intuitive API
  • DRY principle: Eliminates duplicate pagination parsing and resource mapping logic

Testing:

  • All existing tests continue to pass with old destructuring syntax
  • New comprehensive tests verify all three access patterns work correctly
  • Tests cover 6 different paginated resources (users, directories, groups, organizations, connections, vault objects)

Confidence Score: 5/5

  • This PR is safe to merge with no risk
  • The implementation is well-designed with full backwards compatibility maintained through ArrayAccess interface. Comprehensive test coverage validates all three access patterns (bare, named, fluent) across all affected methods. The refactoring eliminates code duplication while preserving existing behavior. No security concerns, no breaking changes, and the code quality is excellent with clear documentation.
  • No files require special attention

Important Files Changed

Filename Overview
lib/Resource/PaginatedResource.php Introduced new PaginatedResource class implementing ArrayAccess and IteratorAggregate for flexible pagination access patterns
lib/DirectorySync.php Refactored listDirectories, listGroups, and listUsers to use PaginatedResource, removing duplicate pagination code
lib/Organizations.php Refactored listOrganizations and listOrganizationFeatureFlags to use PaginatedResource
lib/SSO.php Refactored listConnections to use PaginatedResource
lib/UserManagement.php Refactored listUsers to use PaginatedResource, includes minor formatting improvements to constants
lib/Vault.php Refactored listVaultObjects to use PaginatedResource

Sequence Diagram

sequenceDiagram
    participant Client as Client Code
    participant Service as Service Class<br/>(e.g., UserManagement)
    participant API as WorkOS API
    participant PR as PaginatedResource
    participant Resource as Resource Class<br/>(e.g., User)

    Client->>Service: listUsers(limit, after, etc.)
    Service->>API: GET /user_management/users
    API-->>Service: JSON response with data array<br/>and list_metadata
    Service->>PR: constructFromResponse(response, User::class, 'users')
    PR->>PR: parsePaginationArgs(response)
    Note over PR: Extract before/after cursors
    loop For each item in response["data"]
        PR->>Resource: constructFromResponse(item)
        Resource-->>PR: User object
    end
    PR->>PR: new PaginatedResource(before, after, users, 'users')
    PR-->>Service: PaginatedResource instance
    Service-->>Client: PaginatedResource
    
    Note over Client: Access Pattern 1: Bare destructuring
    Client->>PR: [$before, $after, $users] = result
    PR-->>Client: offsetGet(0), offsetGet(1), offsetGet(2)
    
    Note over Client: Access Pattern 2: Named destructuring
    Client->>PR: ["users" => $users] = result
    PR-->>Client: offsetGet('users')
    
    Note over Client: Access Pattern 3: Fluent access
    Client->>PR: result->users
    PR-->>Client: __get('users')
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@birdcar birdcar requested a review from gjtorikian January 23, 2026 19:29
Previously, when returning data from a resource that was paginated, we
would parse the pagination args off the resposne, loop over the "data"
value in the response, and map each item in the JSON array to a
specific resource. An example of this would be in the listUsers() method
of the UserManagement class (truncated example below):

```php
$users = [];

list($before, $after) = Util\Request::parsePaginationArgs($response);
foreach ($response["data"] as $responseData) {
    \array_push($users, Resource\User::constructFromResponse($responseData));
}

return [$before, $after, $users];
```

Performing this pattern over and over again resulted in a lot of
duplicate code that was doing basically nothing more than an array_map.

Additionally, this return is extremely limited and forces the user into
a limited and specific pattern of either bare array destructuring:

```php
[$before, $after, $users] = $userManagement->listUsers();
```

Or dealing with 0-indexed array values:

```php
$result = $userManagement->listUsers();
```

If for example they just want the first 5 users and don't care about paginating,
this means they'd to either write destructuring that has empty values:

```php
// Huh?
[,,$users] = $userManagement->listUsers(limit: 5);
```

Or they'd have to drop down to

```php
$results = $userManagement->listUsers(limit: 5);
// How do I discover or know what this index is?
$users = $results[2];
```

To fix both of these issues, without affecting current library
consumers, I'm proposing that we create a `Resource\PaginatedResource` class that:

1. DRYs and standardizes the creation of a paginated resource
2. Handles the resource mapping from the data array
3. Continues to allow for bare destructuring (backwards compatible)
4. Introduces named destructuring (e.g `$result["after"]` or
`["users" => $fiveUsers] = $userManagement->listUsers(limit:5)`)
5. Introduces fluent property access (e.g. `$result->after` or
`$result->users`)

The change is fully backwards compatible, cleans up existing resource
code and allows for developers to use the library in whichever code
style is consistent with their project.

For example, it lets you turn this code:

```php
[$before, $after, $users] = $userManagement->listUsers();

while ($after) {
    [$before, $after, $currentPage] = $sso->listConnections(
        limit: 100,
        after: $after,
        order: "desc"
    );

    $users = array_merge($users, $currentPage);
}
```

Into this code:

```php
$users = [];
$after = null;

do {
    $result = $userManagement->listUsers(after: $after, limit: 10);
    $users = array_merge($allUsers, $result->users);
    $after = $result->after;
} while ($after !== null);
```
This was the only paginated method not using the new PaginatedResource
class introduced in 011e80c. Now all paginated methods consistently
return PaginatedResource with support for bare destructuring, named
destructuring, and fluent property access.
@birdcar birdcar force-pushed the birdcar/better-paginated-resources branch from d35cb34 to 1450ea8 Compare January 23, 2026 19:36
@birdcar
Copy link
Contributor Author

birdcar commented Jan 23, 2026

@greptileai

@birdcar
Copy link
Contributor Author

birdcar commented Jan 23, 2026

With 1450ea8, this is now ready to ship imo.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants