Skip to content

Commit cf4044a

Browse files
committed
Added Paginator
1 parent e085db4 commit cf4044a

File tree

3 files changed

+311
-0
lines changed

3 files changed

+311
-0
lines changed

src/Github/Paginator.php

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
namespace Milo\Github;
4+
5+
6+
/**
7+
* Iterates through the Github API responses by Link: header.
8+
*
9+
* @see https://developer.github.com/guides/traversing-with-pagination/
10+
*
11+
* @author Miloslav Hůla (https://github.com/milo)
12+
*/
13+
class Paginator extends Sanity implements \Iterator
14+
{
15+
/** @var Api */
16+
private $api;
17+
18+
/** @var Http\Request */
19+
private $firstRequest;
20+
21+
/** @var Http\Request|NULL */
22+
private $request;
23+
24+
/** @var Http\Response|NULL */
25+
private $response;
26+
27+
/** @var int */
28+
private $limit;
29+
30+
/** @var int */
31+
private $counter = 0;
32+
33+
34+
public function __construct(Api $api, Http\Request $request)
35+
{
36+
$this->api = $api;
37+
$this->firstRequest = clone $request;
38+
}
39+
40+
41+
/**
42+
* Limits maximum steps of iteration.
43+
*
44+
* @param int|NULL
45+
* @return self
46+
*/
47+
public function limit($limit)
48+
{
49+
$this->limit = $limit === NULL
50+
? NULL
51+
: (int) $limit;
52+
53+
return $this;
54+
}
55+
56+
57+
/**
58+
* @return void
59+
*/
60+
public function rewind()
61+
{
62+
$this->request = $this->firstRequest;
63+
$this->response = NULL;
64+
$this->counter = 0;
65+
}
66+
67+
68+
/**
69+
* @return bool
70+
*/
71+
public function valid()
72+
{
73+
return $this->request !== NULL && ($this->limit === NULL || $this->counter < $this->limit);
74+
}
75+
76+
77+
/**
78+
* @return Http\Response
79+
*/
80+
public function current()
81+
{
82+
$this->load();
83+
return $this->response;
84+
}
85+
86+
87+
/**
88+
* @return int
89+
*/
90+
public function key()
91+
{
92+
return static::parsePage($this->request->getUrl());
93+
}
94+
95+
96+
/**
97+
* @return void
98+
*/
99+
public function next()
100+
{
101+
$this->load();
102+
103+
if ($url = static::parseLink($this->response->getHeader('Link'), 'next')) {
104+
$this->request = new Http\Request(
105+
$this->request->getMethod(),
106+
$url,
107+
$this->request->getHeaders(),
108+
$this->request->getContent()
109+
);
110+
} else {
111+
$this->request = NULL;
112+
}
113+
114+
$this->response = NULL;
115+
$this->counter++;
116+
}
117+
118+
119+
private function load()
120+
{
121+
if ($this->response === NULL) {
122+
$this->response = $this->api->request($this->request);
123+
}
124+
}
125+
126+
127+
/**
128+
* @param string
129+
* @return int
130+
*/
131+
public static function parsePage($url)
132+
{
133+
list (, $parametersStr) = explode('?', $url, 2) + ['', ''];
134+
parse_str($parametersStr, $parameters);
135+
136+
return isset($parameters['page'])
137+
? max(1, (int) $parameters['page'])
138+
: 1;
139+
}
140+
141+
142+
/**
143+
* @see https://developer.github.com/guides/traversing-with-pagination/#navigating-through-the-pages
144+
*
145+
* @param string
146+
* @param string
147+
* @return string|NULL
148+
*/
149+
public static function parseLink($link, $rel)
150+
{
151+
if (!preg_match('(<([^>]+)>;\s*rel="' . preg_quote($rel) . '")', $link, $match)) {
152+
return NULL;
153+
}
154+
155+
return $match[1];
156+
}
157+
158+
}

src/github-api.php

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@
2424
require __DIR__ . '/Github/OAuth/Login.php';
2525

2626
require __DIR__ . '/Github/Api.php';
27+
require __DIR__ . '/Github/Paginator.php';

tests/Github/Paginator.phpt

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
/**
4+
* @author Miloslav Hůla
5+
*
6+
* @testCase
7+
*/
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
11+
12+
class MockApi extends Milo\Github\Api
13+
{
14+
/** @var callable */
15+
public $onRequest;
16+
17+
/** @return Milo\Github\Http\Response */
18+
public function request(Milo\Github\Http\Request $request)
19+
{
20+
return call_user_func($this->onRequest, $request);
21+
}
22+
}
23+
24+
25+
class PaginatorTestCase extends Tester\TestCase
26+
{
27+
/** @var MockApi */
28+
private $api;
29+
30+
public function setUp()
31+
{
32+
$this->api = new MockApi;
33+
}
34+
35+
36+
public function testBasics()
37+
{
38+
$responses = [
39+
$r1 = new Milo\Github\Http\Response(200, ['Link' => '<url://test?page=2>; rel="next"'], 'page-1'),
40+
$r2 = new Milo\Github\Http\Response(200, ['Link' => '<url://test?page=3>; rel="next"'], 'page-2'),
41+
$r3 = new Milo\Github\Http\Response(200, [], 'page-3'),
42+
$r4 = new Milo\Github\Http\Response(200, [], 'page-4'),
43+
];
44+
45+
$paginator = new Milo\Github\Paginator($this->api, new Milo\Github\Http\Request(
46+
'METHOD',
47+
'url://test'
48+
));
49+
50+
$requests = [];
51+
$this->api->onRequest = function(Milo\Github\Http\Request $request) use (& $requests, & $responses) {
52+
$requests[] = $request;
53+
return array_shift($responses);
54+
};
55+
56+
$keys = $values = [];
57+
foreach ($paginator as $k => $v) {
58+
$keys[] = $k;
59+
$values[] = $v;
60+
}
61+
62+
Assert::same([$r1, $r2, $r3], $values);
63+
Assert::same([1, 2, 3], $keys);
64+
65+
Assert::same(3, count($requests));
66+
Assert::same('url://test', $requests[0]->getUrl());
67+
Assert::same('url://test?page=2', $requests[1]->getUrl());
68+
Assert::same('url://test?page=3', $requests[2]->getUrl());
69+
Assert::same('METHOD', $requests[0]->getMethod());
70+
Assert::same('METHOD', $requests[1]->getMethod());
71+
Assert::same('METHOD', $requests[2]->getMethod());
72+
}
73+
74+
75+
public function testParsePage()
76+
{
77+
Assert::same(1, Milo\Github\Paginator::parsePage('url://test?page=1'));
78+
Assert::same(2, Milo\Github\Paginator::parsePage('url://test?page=2'));
79+
80+
Assert::same(1, Milo\Github\Paginator::parsePage('url://test'));
81+
Assert::same(1, Milo\Github\Paginator::parsePage('url://test?page='));
82+
Assert::same(1, Milo\Github\Paginator::parsePage('url://test?page=0'));
83+
Assert::same(1, Milo\Github\Paginator::parsePage('url://test?page=foo'));
84+
}
85+
86+
87+
public function testParseLink()
88+
{
89+
Assert::same('url://test', Milo\Github\Paginator::parseLink('<url://test>; rel="foo"', 'foo'));
90+
Assert::same('url://test', Milo\Github\Paginator::parseLink('<url://test>;rel="foo"', 'foo'));
91+
Assert::same('url://test', Milo\Github\Paginator::parseLink("<url://test>;\r\n\trel=\"foo\"", 'foo'));
92+
Assert::same('url://test', Milo\Github\Paginator::parseLink("foo\n<url://test>; rel=\"foo\"", 'foo'));
93+
94+
$link = '<url://a>; rel="a",'."\n\t".'<url://b>; rel="b",'."\n\t".'<url://c>; rel="c"';
95+
Assert::same('url://a', Milo\Github\Paginator::parseLink($link, 'a'));
96+
Assert::same('url://b', Milo\Github\Paginator::parseLink($link, 'b'));
97+
Assert::same('url://c', Milo\Github\Paginator::parseLink($link, 'c'));
98+
99+
Assert::same(NULL, Milo\Github\Paginator::parseLink('', ''));
100+
Assert::same(NULL, Milo\Github\Paginator::parseLink('<url://test>; rel="foo"', 'bar'));
101+
}
102+
103+
104+
public function testLimit()
105+
{
106+
$responses = [
107+
$r1 = new Milo\Github\Http\Response(200, ['Link' => '<url://test?page=21>; rel="next"'], 'page-20'),
108+
$r2 = new Milo\Github\Http\Response(200, ['Link' => '<url://test?page=22>; rel="next"'], 'page-21'),
109+
$r3 = new Milo\Github\Http\Response(200, [], 'page-22'),
110+
];
111+
112+
$paginator = new Milo\Github\Paginator($this->api, new Milo\Github\Http\Request(
113+
'METHOD',
114+
'url://test?page=20'
115+
));
116+
117+
$this->api->onRequest = function(Milo\Github\Http\Request $request) use (& $requests, & $stack) {
118+
$requests[] = $request;
119+
return array_shift($stack);
120+
};
121+
122+
123+
$requests = $values = [];
124+
$stack = $responses;
125+
foreach ($paginator->limit(1) as $v) {
126+
$values[] = $v;
127+
}
128+
Assert::same([$r1], $values);
129+
Assert::same(1, count($requests));
130+
131+
132+
$requests = $values = [];
133+
$stack = $responses;
134+
foreach ($paginator->limit(2) as $v) {
135+
$values[] = $v;
136+
}
137+
Assert::same([$r1, $r2], $values);
138+
Assert::same(2, count($requests));
139+
140+
141+
$requests = $values = [];
142+
$stack = $responses;
143+
foreach ($paginator->limit(0) as $v) {
144+
$values[] = $v;
145+
}
146+
Assert::same([], $values);
147+
Assert::same(0, count($requests));
148+
}
149+
150+
}
151+
152+
(new PaginatorTestCase())->run();

0 commit comments

Comments
 (0)