Skip to content

Add a ResultPager object to support pagination of all Api's #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 30, 2013
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 44 additions & 0 deletions doc/result_pager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## 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 );
$result = $paginator->fetchAll( $organizationApi, 'repositories', 'github );
Copy link
Contributor

Choose a reason for hiding this comment

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

missing closing quote

```

Get the first page
```php
$client = new Github\Client();

$organizationApi = $client->api('organization');

$paginator = new Github\ResultPager( $client );
$result = $paginator->fetch( $organizationApi, 'repositories', 'github );
```
Check for a next page:
```php
$paginator->hasNext();
```

Get next page:
```php
$paginator->fetchNext();
```

Check for pervious page:
```php
$paginator->getPrevious();
Copy link
Contributor

Choose a reason for hiding this comment

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

hasPrevious ?

```

Get prevrious page:
```php
$paginator->fetchPrevious();
```
27 changes: 27 additions & 0 deletions lib/Github/Api/AbstractApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ abstract class AbstractApi implements ApiInterface
*/
protected $client;

/**
* number of items per page (GitHub pagination)
*
* @var int
*/
protected $perPage = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

= null is not needed as it is already the behavior of PHP


/**
* @param Client $client
*/
Expand All @@ -30,11 +37,31 @@ public function configure()
{
}

/**
* @return int|null
*/
public function getPerPage()
{
return $this->perPage;
}

/**
* @param Client $client
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks wrong

*/
public function setPerPage($perPage)
{
$this->perPage = (int) $perPage;
Copy link
Contributor

Choose a reason for hiding this comment

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

this casting forbids setting it back to null

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();
Expand Down
4 changes: 3 additions & 1 deletion lib/Github/HttpClient/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) AND 0 !== strpos( $path, $this->options['base_url'] )) {
Copy link
Contributor

Choose a reason for hiding this comment

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

&&

$path = trim($this->options['base_url'].$path, '/');
}

$request = $this->createRequest($httpMethod, $path);
$request->addHeaders($headers);
Expand Down
157 changes: 157 additions & 0 deletions lib/Github/ResultPager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Github;

use Github\Api\ApiInterface;
use Github\Exception\InvalidArgumentException;
use Github\HttpClient\HttpClient;
use Github\HttpClient\HttpClientInterface;

/**
* Pager class for supporting pagination in github classes
*
* @author Ramon de la Fuente <ramon@future500.nl>
* @author Mitchel Verschoof <mitchel@future500.nl>
*/
class ResultPager implements ResultPagerInterface
{
/**
* @var Github\Client client
*/
protected $client;

/**
* @var array pagination
* Comes from pagination headers in Github API results
*/
protected $pagination = null;

public function __construct( Client $client )
{
$this->client = $client;
}

/**
* @return null|array pagination result of last request
*/
public function getPagination()
{
return $this->pagination;
}

/**
* {@inheritdoc}
*/
public function fetch( ApiInterface $api, $method )
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be cleaner to define public function fetch( ApiInterface $api, $method, array $arguments = array()) than relying on func_get_args IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof I had that initially, but most Api calls only have one parameter so you'd end up having to put array('param') around everything. Also, it stays closer to the api documentation this way.

{
$parameters = array_slice(func_get_args(),2);

$result = call_user_func_array(array($api, $method), $parameters);
$this->postFetch();

return $result;
}

/**
* {@inheritdoc}
*/
public function fetchAll( ApiInterface $api, $method )
{
$parameters = array_slice(func_get_args(),2);

// Set parameters per_page to GitHub max to minimize number of requests
$api->setPerPage(100);
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't you reset it to its current value at the end to avoid side effects ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof Good point. I decided to raise the parameter because it did not make sense to do more api calls than needed. But we'd like to add a resultLimit() to allow fetchAll to be used even when you don't literally want "all". And in that case you might want to keep control over the per_page count as well.


$result = array();
$result = call_user_func_array(array($api, $method), $parameters);
$this->postFetch();

while ($this->hasNext()) {
$result = array_merge($result, $this->fetchNext());
}

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)
{
if (!empty($this->pagination) and isset($this->pagination[$key])) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you should use && instead of and for boolean comparisons. and is a control flow operator

Copy link
Contributor

Choose a reason for hiding this comment

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

and the method can be simplified:

return !empty($this->pagination) && isset($this->pagination[$key]);

return true;
}

return false;
}

/**
* {@inheritdoc}
*/
protected function get($key)
{
if ( $this->has($key) ) {
$result = $this->client->getHttpClient()->get($this->pagination[$key]);
$this->postFetch();

return $result->getContent();
}
}

}
66 changes: 66 additions & 0 deletions lib/Github/ResultPagerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Github;

use Github\Api\ApiInterface;

/**
* Pager interface
*
* @author Ramon de la Fuente <ramon@future500.nl>
* @author Mitchel Verschoof <mitchel@future500.nl>
*/
interface ResultPagerInterface
{
/**
* Fetch a single result (page) from an api call
Copy link
Contributor

Choose a reason for hiding this comment

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

you should document the return value in the methods

*/
public function fetch( ApiInterface $api, $method );

/**
* Fetch all results (pages) from an api call
* Use with care - there is no maximum
*/
public function fetchAll( ApiInterface $api, $method );

/**
* Method that performs the actual work to refresh the pagination property
*/
public function postFetch();
Copy link
Contributor

Choose a reason for hiding this comment

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

should it really be a public method ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof It wasn't - but thinking about how people would interact with the resultPager across requests makes is necesary at this point. If you request the pagination information from the pager i.e. getPagination() you could use a "next" button on your own interface.
But Github does not advise constructing your own urls, just use the full url provided in the link headers. So you'd have to perform a httpclient->get('nextUrl') request, after which you inject the client into the pager and call postFetch once from the outside.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps a public setPagination() that gets called by protected 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();
}
44 changes: 44 additions & 0 deletions test/Github/Tests/Mock/TestResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Github\Tests\Mock;

class TestResponse
{
protected $loopCount;

protected $content;

public function __construct( $loopCount, array $content = array() )
{
$this->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;
}
}
Loading