Skip to content

Commit d35cb34

Browse files
committed
Introduce a dedicated PaginatedResource object
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); ```
1 parent ffffb1a commit d35cb34

File tree

11 files changed

+1105
-456
lines changed

11 files changed

+1105
-456
lines changed

lib/DirectorySync.php

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class DirectorySync
2424
*
2525
* @throws Exception\WorkOSException
2626
*
27-
* @return array{?string, ?string, Resource\Directory[]} An array containing the Directory ID to use as before and after cursor, and an array of Directory instances
27+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and directories array.
28+
* Supports: [$before, $after, $directories] = $result, ["directories" => $dirs] = $result, $result->directories
2829
*/
2930
public function listDirectories(
3031
?string $domain = null,
@@ -54,13 +55,7 @@ public function listDirectories(
5455
true
5556
);
5657

57-
$directories = [];
58-
list($before, $after) = Util\Request::parsePaginationArgs($response);
59-
foreach ($response["data"] as $responseData) {
60-
\array_push($directories, Resource\Directory::constructFromResponse($responseData));
61-
}
62-
63-
return [$before, $after, $directories];
58+
return Resource\PaginatedResource::constructFromResponse($response, Resource\Directory::class, 'directories');
6459
}
6560

6661
/**
@@ -75,7 +70,8 @@ public function listDirectories(
7570
*
7671
* @throws Exception\WorkOSException
7772
*
78-
* @return array{?string, ?string, Resource\DirectoryGroup[]} An array containing the Directory Group ID to use as before and after cursor, and an array of Directory Group instances
73+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and groups array.
74+
* Supports: [$before, $after, $groups] = $result, ["groups" => $groups] = $result, $result->groups
7975
*/
8076
public function listGroups(
8177
?string $directory = null,
@@ -108,13 +104,7 @@ public function listGroups(
108104
true
109105
);
110106

111-
$groups = [];
112-
list($before, $after) = Util\Request::parsePaginationArgs($response);
113-
foreach ($response["data"] as $response) {
114-
\array_push($groups, Resource\DirectoryGroup::constructFromResponse($response));
115-
}
116-
117-
return [$before, $after, $groups];
107+
return Resource\PaginatedResource::constructFromResponse($response, Resource\DirectoryGroup::class, 'groups');
118108
}
119109

120110
/**
@@ -151,7 +141,8 @@ public function getGroup($directoryGroup)
151141
* @param null|string $after Directory User ID to look after
152142
* @param Resource\Order $order The Order in which to paginate records
153143
*
154-
* @return array{?string, ?string, Resource\DirectoryUser[]} An array containing the Directory User ID to use as before and after cursor, and an array of Directory User instances
144+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and users array.
145+
* Supports: [$before, $after, $users] = $result, ["users" => $users] = $result, $result->users
155146
*
156147
* @throws Exception\WorkOSException
157148
*/
@@ -186,13 +177,7 @@ public function listUsers(
186177
true
187178
);
188179

189-
$users = [];
190-
list($before, $after) = Util\Request::parsePaginationArgs($response);
191-
foreach ($response["data"] as $response) {
192-
\array_push($users, Resource\DirectoryUser::constructFromResponse($response));
193-
}
194-
195-
return [$before, $after, $users];
180+
return Resource\PaginatedResource::constructFromResponse($response, Resource\DirectoryUser::class, 'users');
196181
}
197182

198183
/**

lib/Organizations.php

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class Organizations
2121
* @param null|string $after Organization ID to look after
2222
* @param Resource\Order $order The Order in which to paginate records
2323
*
24-
* @return array{?string, ?string, Resource\Organization[]} An array containing the Organization ID to use as before and after cursor, and an array of Organization instances
24+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and organizations array.
25+
* Supports: [$before, $after, $organizations] = $result, ["organizations" => $orgs] = $result, $result->organizations
2526
*
2627
* @throws Exception\WorkOSException
2728
*/
@@ -49,13 +50,7 @@ public function listOrganizations(
4950
true
5051
);
5152

52-
$organizations = [];
53-
list($before, $after) = Util\Request::parsePaginationArgs($response);
54-
foreach ($response["data"] as $responseData) {
55-
\array_push($organizations, Resource\Organization::constructFromResponse($responseData));
56-
}
57-
58-
return [$before, $after, $organizations];
53+
return Resource\PaginatedResource::constructFromResponse($response, Resource\Organization::class, 'organizations');
5954
}
6055

6156
/**
@@ -262,7 +257,8 @@ public function listOrganizationRoles($organizationId)
262257
*
263258
* @throws Exception\WorkOSException
264259
*
265-
* @return array{?string, ?string, Resource\FeatureFlag[]} An array containing the FeatureFlag ID to use as before and after cursor, and an array of FeatureFlag instances
260+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and feature_flags array.
261+
* Supports: [$before, $after, $flags] = $result, ["feature_flags" => $flags] = $result, $result->feature_flags
266262
*/
267263
public function listOrganizationFeatureFlags(
268264
$organizationId,
@@ -287,12 +283,6 @@ public function listOrganizationFeatureFlags(
287283
true
288284
);
289285

290-
$featureFlags = [];
291-
list($before, $after) = Util\Request::parsePaginationArgs($response);
292-
foreach ($response["data"] as $responseData) {
293-
\array_push($featureFlags, Resource\FeatureFlag::constructFromResponse($responseData));
294-
}
295-
296-
return [$before, $after, $featureFlags];
286+
return Resource\PaginatedResource::constructFromResponse($response, Resource\FeatureFlag::class, 'feature_flags');
297287
}
298288
}

lib/Resource/PaginatedResource.php

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
namespace WorkOS\Resource;
4+
5+
/**
6+
* Class PaginatedResource
7+
*
8+
* A flexible paginated resource that supports multiple access patterns:
9+
* 1. Bare destructuring (backwards compatible): [$before, $after, $data] = $result
10+
* 2. Named destructuring: ["users" => $users, "after" => $after] = $result
11+
* 3. Fluent property access: $result->users, $result->after, $result->before
12+
*
13+
* This class standardizes pagination across all WorkOS resources while maintaining
14+
* backwards compatibility with existing code.
15+
*/
16+
class PaginatedResource implements \ArrayAccess, \IteratorAggregate
17+
{
18+
/**
19+
* @var ?string Before cursor for pagination
20+
*/
21+
private $before;
22+
23+
/**
24+
* @var ?string After cursor for pagination
25+
*/
26+
private $after;
27+
28+
/**
29+
* @var array The paginated data items
30+
*/
31+
private $data;
32+
33+
/**
34+
* @var string The key name for the data array (e.g., 'users', 'directories')
35+
*/
36+
private $dataKey;
37+
38+
/**
39+
* PaginatedResource constructor.
40+
*
41+
* @param ?string $before Before cursor
42+
* @param ?string $after After cursor
43+
* @param array $data Array of resource objects
44+
* @param string $dataKey The key name for accessing the data
45+
*/
46+
public function __construct(?string $before, ?string $after, array $data, string $dataKey)
47+
{
48+
$this->before = $before;
49+
$this->after = $after;
50+
$this->data = $data;
51+
$this->dataKey = $dataKey;
52+
}
53+
54+
/**
55+
* Construct a PaginatedResource from an API response
56+
*
57+
* @param array $response The API response containing 'data', 'list_metadata', etc.
58+
* @param string $resourceClass The fully qualified class name of the resource type
59+
* @param string $dataKey The key name for the data array (e.g., 'users', 'directories')
60+
* @return self
61+
*/
62+
public static function constructFromResponse(array $response, string $resourceClass, string $dataKey): self
63+
{
64+
$data = [];
65+
list($before, $after) = \WorkOS\Util\Request::parsePaginationArgs($response);
66+
67+
foreach ($response["data"] as $responseData) {
68+
\array_push($data, $resourceClass::constructFromResponse($responseData));
69+
}
70+
71+
return new self($before, $after, $data, $dataKey);
72+
}
73+
74+
/**
75+
* Magic getter for fluent property access
76+
*
77+
* @param string $name Property name
78+
* @return mixed
79+
*/
80+
public function __get(string $name)
81+
{
82+
if ($name === 'before') {
83+
return $this->before;
84+
}
85+
86+
if ($name === 'after') {
87+
return $this->after;
88+
}
89+
90+
if ($name === 'data' || $name === $this->dataKey) {
91+
return $this->data;
92+
}
93+
94+
return null;
95+
}
96+
97+
/**
98+
* ArrayAccess: Check if offset exists
99+
*
100+
* @param mixed $offset
101+
* @return bool
102+
*/
103+
#[\ReturnTypeWillChange]
104+
public function offsetExists($offset): bool
105+
{
106+
// Support numeric indices for bare destructuring
107+
if (is_int($offset)) {
108+
return $offset >= 0 && $offset <= 2;
109+
}
110+
111+
// Support named keys for named destructuring
112+
return in_array($offset, ['before', 'after', 'data', $this->dataKey], true);
113+
}
114+
115+
/**
116+
* ArrayAccess: Get value at offset
117+
*
118+
* @param mixed $offset
119+
* @return mixed
120+
*/
121+
#[\ReturnTypeWillChange]
122+
public function offsetGet($offset)
123+
{
124+
// Support numeric indices for bare destructuring: [0 => before, 1 => after, 2 => data]
125+
if ($offset === 0) {
126+
return $this->before;
127+
}
128+
129+
if ($offset === 1) {
130+
return $this->after;
131+
}
132+
133+
if ($offset === 2) {
134+
return $this->data;
135+
}
136+
137+
// Support named keys for named destructuring
138+
if ($offset === 'before') {
139+
return $this->before;
140+
}
141+
142+
if ($offset === 'after') {
143+
return $this->after;
144+
}
145+
146+
if ($offset === 'data' || $offset === $this->dataKey) {
147+
return $this->data;
148+
}
149+
150+
return null;
151+
}
152+
153+
/**
154+
* ArrayAccess: Set value at offset (not supported)
155+
*
156+
* @param mixed $offset
157+
* @param mixed $value
158+
* @return void
159+
*/
160+
#[\ReturnTypeWillChange]
161+
public function offsetSet($offset, $value): void
162+
{
163+
throw new \BadMethodCallException('PaginatedResource is immutable');
164+
}
165+
166+
/**
167+
* ArrayAccess: Unset offset (not supported)
168+
*
169+
* @param mixed $offset
170+
* @return void
171+
*/
172+
#[\ReturnTypeWillChange]
173+
public function offsetUnset($offset): void
174+
{
175+
throw new \BadMethodCallException('PaginatedResource is immutable');
176+
}
177+
178+
/**
179+
* IteratorAggregate: Get iterator for the data array
180+
*
181+
* @return \ArrayIterator
182+
*/
183+
public function getIterator(): \Traversable
184+
{
185+
return new \ArrayIterator($this->data);
186+
}
187+
188+
/**
189+
* Magic isset for property checking
190+
*
191+
* @param string $name
192+
* @return bool
193+
*/
194+
public function __isset(string $name): bool
195+
{
196+
return in_array($name, ['before', 'after', 'data', $this->dataKey], true);
197+
}
198+
}

lib/SSO.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ public function getConnection($connection)
214214
* @param null|string $after Connection ID to look after
215215
* @param Resource\Order $order The Order in which to paginate records
216216
*
217-
* @return array{?string, ?string, Resource\Connection[]} An array containing the Directory Connection ID to use as before and after cursor, and an array of Connection instances
217+
* @return Resource\PaginatedResource A paginated resource containing before/after cursors and connections array.
218+
* Supports: [$before, $after, $connections] = $result, ["connections" => $connections] = $result, $result->connections
218219
*
219220
* @throws Exception\WorkOSException
220221
*/
@@ -246,12 +247,6 @@ public function listConnections(
246247
true
247248
);
248249

249-
$connections = [];
250-
list($before, $after) = Util\Request::parsePaginationArgs($response);
251-
foreach ($response["data"] as $responseData) {
252-
\array_push($connections, Resource\Connection::constructFromResponse($responseData));
253-
}
254-
255-
return [$before, $after, $connections];
250+
return Resource\PaginatedResource::constructFromResponse($response, Resource\Connection::class, 'connections');
256251
}
257252
}

0 commit comments

Comments
 (0)