Skip to content

Commit bf88702

Browse files
authored
Merge pull request #70 from microsoftgraph/feat/page-iterator
Add page iterator support.
2 parents b949fc1 + e6c2811 commit bf88702

File tree

6 files changed

+603
-1
lines changed

6 files changed

+603
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"microsoft/kiota-serialization-text": "^0.2.0",
2020
"microsoft/kiota-abstractions": "^0.2.0",
2121
"php-http/httplug": "^2.2",
22-
"php-http/guzzle7-adapter": "^1.0"
22+
"php-http/guzzle7-adapter": "^1.0",
23+
"ext-json": "*"
2324
},
2425
"require-dev": {
2526
"phpunit/phpunit": "^9.0",

src/Models/PageResult.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Microsoft\Graph\Core\Models;
4+
5+
class PageResult
6+
{
7+
/** @var string|null $odataNextLink */
8+
private ?string $odataNextLink;
9+
/** @var array<mixed>|null $value */
10+
private ?array $value;
11+
12+
/**
13+
* @return string|null
14+
*/
15+
public function getOdataNextLink(): ?string {
16+
return $this->odataNextLink;
17+
}
18+
19+
/**
20+
* @return array<mixed>|null
21+
*/
22+
public function getValue(): ?array {
23+
return $this->value;
24+
}
25+
26+
/**
27+
* @param string|null $nextLink
28+
*/
29+
public function setOdataNextLink(?string $nextLink): void{
30+
$this->odataNextLink = $nextLink;
31+
}
32+
33+
/**
34+
* @param array|null $value
35+
*/
36+
public function setValue(?array $value): void {
37+
$this->value = $value;
38+
}
39+
}

src/Tasks/PageIterator.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
namespace Microsoft\Graph\Core\Tasks;
4+
5+
use Exception;
6+
use Http\Promise\FulfilledPromise;
7+
use Http\Promise\Promise;
8+
use Http\Promise\RejectedPromise;
9+
use InvalidArgumentException;
10+
use JsonException;
11+
use Microsoft\Graph\Core\Models\PageResult;
12+
use Microsoft\Kiota\Abstractions\HttpMethod;
13+
use Microsoft\Kiota\Abstractions\NativeResponseHandler;
14+
use Microsoft\Kiota\Abstractions\RequestAdapter;
15+
use Microsoft\Kiota\Abstractions\RequestInformation;
16+
use Microsoft\Kiota\Abstractions\RequestOption;
17+
use Microsoft\Kiota\Abstractions\Serialization\Parsable;
18+
19+
class PageIterator
20+
{
21+
private PageResult $currentPage;
22+
private RequestAdapter $requestAdapter;
23+
private bool $hasNext = false;
24+
private int $pauseIndex;
25+
/** @var array{string, string} $constructorFunc */
26+
private array $constructorCallable;
27+
private array $headers = [];
28+
/** @var array<RequestOption>|null */
29+
private ?array $requestOptions = [];
30+
31+
/**
32+
* @param Parsable|array|object $response paged collection response
33+
* @param RequestAdapter $requestAdapter
34+
* @param array{string,string} $constructorCallable The method to construct a paged response object.
35+
* @throws JsonException
36+
*/
37+
public function __construct($response, RequestAdapter $requestAdapter, array $constructorCallable) {
38+
$this->requestAdapter = $requestAdapter;
39+
$this->constructorCallable = $constructorCallable;
40+
$this->pauseIndex = 0;
41+
$page = self::convertToPage($response);
42+
43+
if ($page !== null) {
44+
$this->currentPage = $page;
45+
$this->hasNext = true;
46+
}
47+
$this->headers = [];
48+
}
49+
50+
/**
51+
* @param array $headers
52+
*/
53+
public function setHeaders(array $headers): void
54+
{
55+
$this->headers = $headers;
56+
}
57+
58+
/**
59+
* @param array $requestOptions
60+
*/
61+
public function setRequestOptions(array $requestOptions): void
62+
{
63+
$this->requestOptions = $requestOptions;
64+
}
65+
66+
/**
67+
* @param int $pauseIndex
68+
*/
69+
public function setPauseIndex(int $pauseIndex): void
70+
{
71+
$this->pauseIndex = $pauseIndex;
72+
}
73+
74+
/**
75+
* @param callable(Parsable|array|object): bool $callback The callback function to apply on every entity. Pauses iteration if false is returned
76+
* @throws Exception
77+
*/
78+
public function iterate(callable $callback): void {
79+
while(true) {
80+
$keepIterating = $this->enumerate($callback);
81+
82+
if (!$keepIterating) {
83+
return;
84+
}
85+
$nextPage = $this->next();
86+
87+
if (empty($nextPage)) {
88+
$this->hasNext = false;
89+
return;
90+
}
91+
$this->currentPage = $nextPage;
92+
$this->pauseIndex = 0;
93+
}
94+
}
95+
96+
/**
97+
* @throws Exception
98+
*/
99+
public function next(): ?PageResult {
100+
if (empty($this->currentPage->getOdataNextLink())) {
101+
return null;
102+
}
103+
104+
$response = $this->fetchNextPage();
105+
$result = $response->wait();
106+
return self::convertToPage($result);
107+
}
108+
109+
/**
110+
* @param $response
111+
* @return PageResult|null
112+
* @throws JsonException
113+
*/
114+
public static function convertToPage($response): ?PageResult {
115+
$page = new PageResult();
116+
if ($response === null) {
117+
throw new InvalidArgumentException('$response cannot be null');
118+
}
119+
120+
if (is_array($response)) {
121+
$value = $response['value'];
122+
} else if(is_a($response, Parsable::class) && method_exists($response, 'getValue')) {
123+
$value = $response->getValue();
124+
} else {
125+
$value = $response->value;
126+
}
127+
128+
if ($value === null) {
129+
throw new InvalidArgumentException('The response does not contain a value.');
130+
}
131+
132+
$parsablePage = is_a($response, Parsable::class) ? $response : json_decode(json_encode($response,JSON_THROW_ON_ERROR), true);
133+
if (is_array($parsablePage)) {
134+
$page->setOdataNextLink($parsablePage['@odata.nextLink'] ?? '');
135+
} else {
136+
$page->setOdataNextLink($parsablePage->getOdataNextLink());
137+
}
138+
$page->setValue($value);
139+
return $page;
140+
}
141+
private function fetchNextPage(): ?Promise {
142+
/** @var Parsable $graphResponse */
143+
$graphResponse = null;
144+
145+
$nextLink = $this->currentPage->getOdataNextLink();
146+
147+
if ($nextLink === null) {
148+
return new RejectedPromise(new InvalidArgumentException('The response does not have a nextLink'));
149+
}
150+
151+
if (!filter_var($nextLink, FILTER_VALIDATE_URL)) {
152+
throw new InvalidArgumentException('Could not parse the nextLink url.');
153+
}
154+
155+
$requestInfo = new RequestInformation();
156+
$requestInfo->httpMethod = HttpMethod::GET;
157+
$requestInfo->setUri($nextLink);
158+
$requestInfo->headers = $this->headers;
159+
if ($this->requestOptions !== null) {
160+
$requestInfo->addRequestOptions(...$this->requestOptions);
161+
}
162+
163+
return $this->requestAdapter->sendAsync($requestInfo, $this->constructorCallable);
164+
}
165+
166+
public function enumerate(?callable $callback): ?bool {
167+
$keepIterating = true;
168+
169+
$pageItems = $this->currentPage->getValue();
170+
if (empty($pageItems)) {
171+
return false;
172+
}
173+
for ($i = $this->pauseIndex; $i < count($pageItems); $i++){
174+
$keepIterating = $callback($pageItems[$i]);
175+
176+
if (!$keepIterating) {
177+
$this->pauseIndex = $i + 1;
178+
break;
179+
}
180+
}
181+
return $keepIterating;
182+
}
183+
184+
public function hasNext(): bool {
185+
return $this->hasNext;
186+
}
187+
}

0 commit comments

Comments
 (0)