diff --git a/doc/index.md b/doc/index.md index 648d2cb870c..5a98737f912 100644 --- a/doc/index.md +++ b/doc/index.md @@ -18,6 +18,7 @@ APIs: Additional features: +* [Pagination support](result_pager.md) * [Authentication & Security](security.md) * [Request any Route](request_any_route.md) * [Customize `php-github-api` and testing](customize.md) diff --git a/doc/result_pager.md b/doc/result_pager.md new file mode 100644 index 00000000000..a802be1caa9 --- /dev/null +++ b/doc/result_pager.md @@ -0,0 +1,52 @@ +## Result Pager +[Back to the navigation](index.md) + +### Usage examples + +Get all repositories of a organization + +```php +$client = new Github\Client(); + +$organizationApi = $client->api('organization'); + +$paginator = new Github\ResultPager($client); +$parameters = array('github'); +$result = $paginator->fetchAll($organizationApi, 'repositories', $parameters); +``` + +Get the first page +```php +$client = new Github\Client(); + +$organizationApi = $client->api('organization'); + +$paginator = new Github\ResultPager( $client ); +$parameters = array('github'); +$result = $paginator->fetch($organizationApi, 'repositories', $parameters); +``` + +Check for a next page: +```php +$paginator->hasNext(); +``` + +Get next page: +```php +$paginator->fetchNext(); +``` + +Check for pervious page: +```php +$paginator->hasPrevious(); +``` + +Get prevrious page: +```php +$paginator->fetchPrevious(); +``` + +If you want to retrieve the pagination links (available after the call to fetch): +```php +$paginator->getPagination(); +``` diff --git a/lib/Github/Api/AbstractApi.php b/lib/Github/Api/AbstractApi.php index be6063d3cfb..727f4e51824 100644 --- a/lib/Github/Api/AbstractApi.php +++ b/lib/Github/Api/AbstractApi.php @@ -18,6 +18,13 @@ abstract class AbstractApi implements ApiInterface */ protected $client; + /** + * number of items per page (GitHub pagination) + * + * @var null|int + */ + protected $perPage; + /** * @param Client $client */ @@ -30,11 +37,32 @@ public function configure() { } + /** + * @return null|int + */ + public function getPerPage() + { + return $this->perPage; + } + + /** + * @param null|int $perPage + */ + public function setPerPage($perPage) + { + $this->perPage = (null === $perPage ? $perPage : (int) $perPage); + + return $this; + } + /** * {@inheritDoc} */ protected function get($path, array $parameters = array(), $requestHeaders = array()) { + if (null !== $this->perPage && !isset($parameters['per_page'])) { + $parameters['per_page'] = $this->perPage; + } $response = $this->client->getHttpClient()->get($path, $parameters, $requestHeaders); return $response->getContent(); diff --git a/lib/Github/HttpClient/HttpClient.php b/lib/Github/HttpClient/HttpClient.php index ef82a2a0044..92467bb1e91 100644 --- a/lib/Github/HttpClient/HttpClient.php +++ b/lib/Github/HttpClient/HttpClient.php @@ -163,7 +163,9 @@ public function put($path, array $parameters = array(), array $headers = array() */ public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array()) { - $path = trim($this->options['base_url'].$path, '/'); + if (!empty($this->options['base_url']) && 0 !== strpos($path, $this->options['base_url'])) { + $path = trim($this->options['base_url'].$path, '/'); + } $request = $this->createRequest($httpMethod, $path); $request->addHeaders($headers); diff --git a/lib/Github/ResultPager.php b/lib/Github/ResultPager.php new file mode 100644 index 00000000000..0ce7d11f595 --- /dev/null +++ b/lib/Github/ResultPager.php @@ -0,0 +1,166 @@ + + * @author Mitchel Verschoof + */ +class ResultPager implements ResultPagerInterface +{ + /** + * @var Github\Client client + */ + protected $client; + + /** + * @var array pagination + * Comes from pagination headers in Github API results + */ + protected $pagination; + + + /** + * The Github client to use for pagination. This must be the same + * instance that you got the Api instance from, i.e.: + * + * $client = new \Github\Client(); + * $api = $client->api('someApi'); + * $pager = new \Github\ResultPager($client); + * + * @param \Github\Client $client + * + */ + public function __construct(Client $client) + { + $this->client = $client; + } + + /** + * {@inheritdoc} + */ + public function getPagination() + { + return $this->pagination; + } + + /** + * {@inheritdoc} + */ + public function fetch(ApiInterface $api, $method, array $parameters = array()) + { + $result = call_user_func_array(array($api, $method), $parameters); + $this->postFetch(); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function fetchAll(ApiInterface $api, $method, array $parameters = array()) + { + // get the perPage from the api + $perPage = $api->getPerPage(); + + // Set parameters per_page to GitHub max to minimize number of requests + $api->setPerPage(100); + + $result = array(); + $result = call_user_func_array(array($api, $method), $parameters); + $this->postFetch(); + + while ($this->hasNext()) { + $result = array_merge($result, $this->fetchNext()); + } + + // restore the perPage + $api->setPerPage($perPage); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function postFetch() + { + $this->pagination = $this->client->getHttpClient()->getLastResponse()->getPagination(); + } + + /** + * {@inheritdoc} + */ + public function hasNext() + { + return $this->has('next'); + } + + /** + * {@inheritdoc} + */ + public function fetchNext() + { + return $this->get('next'); + } + + /** + * {@inheritdoc} + */ + public function hasPrevious() + { + return $this->has('prev'); + } + + /** + * {@inheritdoc} + */ + public function fetchPrevious() + { + return $this->get('prev'); + } + + /** + * {@inheritdoc} + */ + public function fetchFirst() + { + return $this->get('first'); + } + + /** + * {@inheritdoc} + */ + public function fetchLast() + { + return $this->get('last'); + } + + /** + * {@inheritdoc} + */ + protected function has($key) + { + return !empty($this->pagination) && isset($this->pagination[$key]); + } + + /** + * {@inheritdoc} + */ + protected function get($key) + { + if ($this->has($key)) { + $result = $this->client->getHttpClient()->get($this->pagination[$key]); + $this->postFetch(); + + return $result->getContent(); + } + } +} diff --git a/lib/Github/ResultPagerInterface.php b/lib/Github/ResultPagerInterface.php new file mode 100644 index 00000000000..6faab5efda2 --- /dev/null +++ b/lib/Github/ResultPagerInterface.php @@ -0,0 +1,84 @@ + + * @author Mitchel Verschoof + */ +interface ResultPagerInterface +{ + + /** + * @return null|array pagination result of last request + */ + public function getPagination(); + + /** + * Fetch a single result (page) from an api call + * + * @param ApiInterface $api the Api instance + * @param string $method the method name to call on the Api instance + * @param array $parameters the method parameters in an array + * + * @return array returns the result of the Api::$method() call + */ + public function fetch(ApiInterface $api, $method, array $parameters = array()); + + /** + * Fetch all results (pages) from an api call + * Use with care - there is no maximum + * + * @param ApiInterface $api the Api instance + * @param string $method the method name to call on the Api instance + * @param array $parameters the method parameters in an array + * + * @return array returns a merge of the results of the Api::$method() call + */ + public function fetchAll(ApiInterface $api, $method, array $parameters = array()); + + /** + * Method that performs the actual work to refresh the pagination property + */ + public function postFetch(); + + /** + * Check to determine the availability of a next page + * @return bool + */ + public function hasNext(); + + /** + * Check to determine the availability of a previous page + * @return bool + */ + public function hasPrevious(); + + /** + * Fetch the next page + * @return array + */ + public function fetchNext(); + + /** + * Fetch the previous page + * @return array + */ + public function fetchPrevious(); + + /** + * Fetch the first page + * @return array + */ + public function fetchFirst(); + + /** + * Fetch the last page + * @return array + */ + public function fetchLast(); +} diff --git a/test/Github/Tests/Mock/TestResponse.php b/test/Github/Tests/Mock/TestResponse.php new file mode 100644 index 00000000000..0c8f04b320e --- /dev/null +++ b/test/Github/Tests/Mock/TestResponse.php @@ -0,0 +1,44 @@ +loopCount = $loopCount; + $this->content = $content; + } + + /** + * {@inheritDoc} + */ + public function getContent() + { + return $this->content; + } + + /** + * @return array|null + */ + public function getPagination() + { + if($this->loopCount){ + $returnArray = array( + 'next' => 'http://github.com/' . $this->loopCount + ); + } else { + $returnArray = array( + 'prev' => 'http://github.com/prev' + ); + } + + $this->loopCount--; + + return $returnArray; + } +} diff --git a/test/Github/Tests/ResultPagerTest.php b/test/Github/Tests/ResultPagerTest.php new file mode 100644 index 00000000000..7d37bb4a957 --- /dev/null +++ b/test/Github/Tests/ResultPagerTest.php @@ -0,0 +1,249 @@ + + * @author Mitchel Verschoof + */ +class ResultPagerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + * + * description fetchAll + */ + public function shouldGetAllResults() + { + $amountLoops = 3; + $content = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + $responseMock = new TestResponse($amountLoops, $content); + + // httpClient mock + $httpClientMock = $this->getHttpClientMock($responseMock); + $httpClientMock + ->expects($this->exactly($amountLoops)) + ->method('get') + ->will($this->returnValue($responseMock)); + + $clientMock = $this->getClientMock($httpClientMock); + + // memberApi Mock + $memberApiMock = $this->getApiMock('Github\Api\Organization\Members'); + $memberApiMock + ->expects($this->once()) + ->method('all') + ->will($this->returnValue(array())); + + $method = 'all'; + $parameters = array('netwerven'); + + // Run fetchAll on result paginator + $paginator = new Github\ResultPager($clientMock); + $result = $paginator->fetchAll($memberApiMock, $method, $parameters); + + $this->assertEquals($amountLoops * count($content), count($result)); + } + + /** + * @test + * + * description fetch + */ + public function shouldGetSomeResults() + { + $pagination = array('next' => 'http://github.com/next'); + $resultContent = 'organization test'; + + $responseMock = $this->getResponseMock($pagination); + $httpClient = $this->getHttpClientMock($responseMock); + $client = $this->getClientMock($httpClient); + + $organizationApiMock = $this->getApiMock('Github\Api\Organization'); + + $organizationApiMock + ->expects($this->once()) + ->method('show') + ->with('github') + ->will($this->returnValue($resultContent)); + + $paginator = new Github\ResultPager($client); + $result = $paginator->fetch($organizationApiMock, 'show', array('github')); + + $this->assertEquals($resultContent, $result); + $this->assertEquals($pagination, $paginator->getPagination()); + } + + /** + * @test + * + * description postFetch + */ + public function postFetch() + { + $pagination = array( + 'first' => 'http://github.com', + 'next' => 'http://github.com', + 'prev' => 'http://github.com', + 'last' => 'http://github.com' + ); + + // response mock + $responseMock = $this->getMock('Github\HttpClient\Message\Response'); + $responseMock + ->expects($this->any()) + ->method('getPagination') + ->will($this->returnValue($pagination)); + + $httpClient = $this->getHttpClientMock($responseMock); + $client = $this->getClientMock($httpClient); + + $paginator = new Github\ResultPager($client); + $paginator->postFetch(); + + $this->assertEquals($paginator->getPagination(), $pagination); + } + + /** + * @test + * + * description fetchNext + */ + public function fetchNext() + { + $pagination = array('next' => 'http://github.com/next'); + $resultContent = 'fetch test'; + + $responseMock = $this->getResponseMock($pagination); + $responseMock + ->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($resultContent)); + // Expected 2 times, 1 for setup and 1 for the actual test + $responseMock + ->expects($this->exactly(2)) + ->method('getPagination'); + + $httpClient = $this->getHttpClientMock($responseMock); + + $httpClient + ->expects($this->once()) + ->method('get') + ->with($pagination['next']) + ->will($this->returnValue($responseMock)); + + $client = $this->getClientMock($httpClient); + + $paginator = new Github\ResultPager($client); + $paginator->postFetch(); + + $this->assertEquals($paginator->fetchNext(), $resultContent); + } + + /** + * @test + * + * description hasNext + */ + public function shouldHaveNext() + { + $responseMock = $this->getResponseMock(array('next' => 'http://github.com/next')); + $httpClient = $this->getHttpClientMock($responseMock); + $client = $this->getClientMock($httpClient); + + $paginator = new Github\ResultPager($client); + $paginator->postFetch(); + + $this->assertEquals($paginator->hasNext(), true); + $this->assertEquals($paginator->hasPrevious(), false); + } + + /** + * @test + * + * description hasPrevious + */ + public function shouldHavePrevious() + { + $responseMock = $this->getResponseMock(array('prev' => 'http://github.com/previous')); + $httpClient = $this->getHttpClientMock($responseMock); + $client = $this->getClientMock($httpClient); + + $paginator = new Github\ResultPager($client); + $paginator->postFetch(); + + $this->assertEquals($paginator->hasPrevious(), true); + $this->assertEquals($paginator->hasNext(), false); + } + + protected function getResponseMock(array $pagination) + { + // response mock + $responseMock = $this->getMock('Github\HttpClient\Message\Response'); + $responseMock + ->expects($this->any()) + ->method('getPagination') + ->will($this->returnValue($pagination)); + + return $responseMock; + } + + protected function getClientMock(HttpClientInterface $httpClient = null) + { + // if no httpClient isset use the default HttpClient mock + if (!$httpClient) { + $httpClient = $this->getHttpClientMock(); + } + + $client = new \Github\Client($httpClient); + $client->setHttpClient($httpClient); + + return $client; + } + + protected function getHttpClientMock($responseMock = null) + { + // mock the client interface + $clientInterfaceMock = $this->getMock('Buzz\Client\ClientInterface', array('setTimeout', 'setVerifyPeer', 'send')); + $clientInterfaceMock + ->expects($this->any()) + ->method('setTimeout') + ->with(10); + $clientInterfaceMock + ->expects($this->any()) + ->method('setVerifyPeer') + ->with(false); + $clientInterfaceMock + ->expects($this->any()) + ->method('send'); + + // create the httpClient mock + $httpClientMock = $this->getMock('Github\HttpClient\HttpClient', array(), array(array(), $clientInterfaceMock)); + + if ($responseMock) { + $httpClientMock + ->expects($this->any()) + ->method('getLastResponse') + ->will($this->returnValue($responseMock)); + } + + return $httpClientMock; + } + + protected function getApiMock($apiClass) + { + $client = $this->getClientMock(); + + return $this->getMockBuilder($apiClass) + ->setConstructorArgs(array($client)) + ->getMock(); + } +}